diff --git a/.htaccess b/.htaccess index 77827ae..8311ccf 100644 --- a/.htaccess +++ b/.htaccess @@ -4,6 +4,11 @@ RewriteEngine On + Header add Access-Control-Allow-Origin "*" + Header add Access-Control-Allow-Methods "GET,PUT, POST, DELETE, OPTIONS" + Header add Access-Control-Allow-Headers "Authorization" + + RewriteRule ^vpu - [L,NC] # Redirect Trailing Slashes... RewriteRule ^(.*)/$ /$1 [L,R=301] @@ -12,4 +17,5 @@ RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] diff --git a/app/config/app.php b/app/config/app.php index 9b91aa3..6112e17 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -106,6 +106,7 @@ 'Illuminate\Validation\ValidationServiceProvider', 'Illuminate\View\ViewServiceProvider', 'Illuminate\Workbench\WorkbenchServiceProvider', + 'Travisjryan\Twilio\TwilioServiceProvider' ), @@ -169,6 +170,7 @@ 'Session' => 'Illuminate\Support\Facades\Session', 'SSH' => 'Illuminate\Support\Facades\SSH', 'Str' => 'Illuminate\Support\Str', + 'Twilio' => 'Travisjryan\Twilio\Facades\Twilio', 'URL' => 'Illuminate\Support\Facades\URL', 'Validator' => 'Illuminate\Support\Facades\Validator', 'View' => 'Illuminate\Support\Facades\View', diff --git a/app/config/packages/travisjryan/twilio/twilio.php b/app/config/packages/travisjryan/twilio/twilio.php new file mode 100644 index 0000000..0780277 --- /dev/null +++ b/app/config/packages/travisjryan/twilio/twilio.php @@ -0,0 +1,18 @@ + "AC3b8cd1e12a5b6ff4e4ea25f431833a97", + + /** + * token - Access token that can be found in your Twilio dashboard + */ + "token" => "22964db1d502e5069c2d44c9a49c8b6c", + + /** + * from - The Phone number registered with Twilio that your SMS & Calls will come from + */ + "from" => "+441332402803" +); \ No newline at end of file diff --git a/app/config/pubnub.php b/app/config/pubnub.php index 99d55fd..c92ddf1 100644 --- a/app/config/pubnub.php +++ b/app/config/pubnub.php @@ -1,8 +1,6 @@ 'pub-c-18bc7bd1-2981-4cc4-9c4e-234d25519d36', 'subscribe_key' => 'sub-c-5782df52-d147-11e3-93dd-02ee2ddab7fe', - -); \ No newline at end of file +); diff --git a/app/controllers/AuthenticationController.php b/app/controllers/AuthenticationController.php new file mode 100644 index 0000000..1679d48 --- /dev/null +++ b/app/controllers/AuthenticationController.php @@ -0,0 +1,32 @@ + "test")); + } + else{ + list($email, $password) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); + } + if (Auth::attempt(array('email' => $email, 'password' => $password))) + { + return Response::json(Auth::user()); + } + + return Response::json(array("id" => 0)); + } + } +?> \ No newline at end of file diff --git a/app/controllers/PrivilegeController.php b/app/controllers/PrivilegeController.php new file mode 100644 index 0000000..4efe352 --- /dev/null +++ b/app/controllers/PrivilegeController.php @@ -0,0 +1,27 @@ +name = Input::get('name'); + + $privilege->save(); + + return Response::json($privilege); + } + + public function deletePrivilege($id) + { + $privilege = Privilege::find($id); + $privilege->delete(); + + return Response::json(array("success" => true)); + } + } +?> \ No newline at end of file diff --git a/app/controllers/PulseController.php b/app/controllers/PulseController.php index 81bcfc7..06cbb9b 100644 --- a/app/controllers/PulseController.php +++ b/app/controllers/PulseController.php @@ -3,7 +3,6 @@ class PulseController extends BaseController{ public function getAll() { - return Response::json(Pulse::all()); } @@ -22,12 +21,20 @@ public function store() $pubnub = App::make('pubnub'); $pubnub->publish(array( - 'channel' => 'Cadence', + 'channel' => 'pulses-' . $pulse->server_id, 'message' => json_decode($pulse) )); return Response::json($pulse); + } + + public function deletePulse($id) + { + $user = Pulse::find($id); + $user->delete(); + return Response::json(array("success" => true)); } + } ?> \ No newline at end of file diff --git a/app/controllers/ServerController.php b/app/controllers/ServerController.php new file mode 100644 index 0000000..7ca4231 --- /dev/null +++ b/app/controllers/ServerController.php @@ -0,0 +1,150 @@ + json_decode($server), 'pulses' => json_decode($server->pulse))); + return Response::json($server); + } + + public function store() + { + $server = new Server; + + $server->servergroup_id = Input::get("servergroup_id"); + $server->name = Input::get("name"); + $server->available_disk = Input::get("available_disk"); + $server->available_ram = Input::get("available_ram"); + $server->cpu_speed = Input::get("cpu_speed"); + $server->os_name = Input::get("os_name"); + $server->os_version = Input::get("os_version"); + $server->guid = Input::get("guid"); + + $server->save(); + + $pubnub = App::make('pubnub'); + $pubnub->publish(array( + 'channel' => 'Cadence', + 'message' => json_decode($server) + )); + + return Response::json($server); + } + + public function getPulses($id) + { + $server = Server::find($id); + return Response::json($server->pulse); + } + + public function getPulsesForDays($id, $days) + { + $server = Server::find($id); + $pulses = Pulse::ofServerID($id)->ofDays($days)->get(); + return Response::json($pulses); + } + + public function getLatestPulse($id) + { + $pulse = Pulse::ofServerID($id)->orderby('timestamp', 'asc')->first(); + return Response::json($pulse); + } + + public function changeStatus($guid) + { + Server::where('guid', '=', $guid)->update(array('online' => Input::get("status"))); + $server = Server::where('guid', '=', $guid)->get(); + + if($server->isEmpty()) + { + return Response::make("Server with this GUID not found"); + } + + $subscriptionsForServer = Subscription::ofServerID($server->first()->servergroup_id)->get(); + foreach ($subscriptionsForServer as $subscription) + { + if (Input::get("status") == 0) + { + if($subscription->text == 1) + { + Twilio::message($subscription->user->mobile_number, "CADENCE STATUS ALERT: Server '" . $server->first()->name . "' has gone offline. Urgent attention needed."); + } + if($subscription->phonecall == 1) + { + Twilio::call($subscription->user->mobile_number, 'http://cadence-bu.cloudapp.net/voicealert.xml'); + } + } + else + { + if($subscription->text == 1) + { + Twilio::message($subscription->user->mobile_number, "CADENCE STATUS UPDATE: Server '" . $server->first()->name . "' has come back online. Have a great day."); + } + } + } + + $pubnub = App::make('pubnub'); + $pubnub->publish(array( + 'channel' => 'pulses-' . $server->first()->id . '-online', + 'message' => json_encode(array("online" => Input::get("status"))) + )); + + return Response::json($server->first()); + } + + public function updateServerDetails($guid) + { + $server = Server::where('guid', '=', $guid)->first()->update(array( + "available_disk" => Input::get("available_disk"), + "available_ram" => Input::get("available_ram"), + "cpu_speed" => Input::get("cpu_speed"), + "os_name" => Input::get("os_name"), + "os_version" => Input::get("os_version") + )); + return Response::json(Server::where('guid', '=', $guid)->get()); + } + + public function updateServerGroup($id) + { + $server = Server::find($id); + + $server->servergroup_id = Input::get('servergroup_id'); + $server->save(); + } + + public function getStatus($guid) + { + $server = Server::where('guid', '=', $guid)->get(); + return Response::json($server); + } + + public function deleteServer($id) + { + $server = Server::find($id); + $server->delete(); + + return Response::json(array("success" => true)); + } + + public function getUnassignedServers() + { + return Response::json(Server::ofUnassigned(0)->get()); + } + + public function getAssignedServers() + { + return Response::json(Server::ofAssigned(0)->get()); + } + + public function getServersForStatus($status) + { + return Response::json(Server::ofStatus($status)->get()); + } + } +?> \ No newline at end of file diff --git a/app/controllers/ServerGroupController.php b/app/controllers/ServerGroupController.php new file mode 100644 index 0000000..ccb5078 --- /dev/null +++ b/app/controllers/ServerGroupController.php @@ -0,0 +1,72 @@ +server); + } + + public function store() + { + if (ServerGroup::where('name','=', Input::get("name"))->count() > 0) { + //$user = User::where('email','=', Input::get("email"))->get()->first(); + return Response::json(array('success' => false , 'error' => 'Server Group already exists')); + } + + $serverGroup = new ServerGroup; + $serverGroup->name = Input::get("name"); + $serverGroup->save(); + + return Response::json($serverGroup); + } + + public function deleteServerGroup($id) + { + $serverGroup = ServerGroup::find($id); + $serverGroup->delete(); + + $servers = Server::where("servergroup_id", "=", $id)->get(); + + foreach($servers as $server) + { + $server->servergroup_id = 0; + $server->save(); + } + + $subscriptions = Subscription::where("servergroup_id", "=", $id)->get(); + + foreach($subscriptions as $Subscription) + { + $Subscription->delete(); + } + + return Response::json(array("success" => true)); + } + + public function getServerGroup($id) + { + return Response::json(ServerGroup::find($id)); + } + + public function editServerGroup($id) + { + if (ServerGroup::find($id)->count() > 0) { + //$user = User::where('email','=', Input::get("email"))->get()->first(); + return Response::json(array('success' => false , 'error' => 'Server Group already exists')); + } + + $serverGroup = ServerGroup::find($id); + $serverGroup->name = Input::get("name"); + + $serverGroup->save(); + + return Response::json($serverGroup); + } + } +?> \ No newline at end of file diff --git a/app/controllers/SubscriptionController.php b/app/controllers/SubscriptionController.php new file mode 100644 index 0000000..31f6b29 --- /dev/null +++ b/app/controllers/SubscriptionController.php @@ -0,0 +1,41 @@ +servergroup_id = Input::get('servergroup_id'); + $subscription->user_id = Input::get('user_id'); + $subscription->phonecall = Input::get('phonecall'); + $subscription->text = Input::get('text'); + $subscription->push = Input::get('push'); + + $subscription->save(); + + return Response::json($subscription); + } + + public function updateSubscription($id) + { + $subscription = Subscription::find($id); + $subscription->text = Input::get('text'); + $subscription->phonecall = Input::get('phonecall'); + $subscription->save(); + + return Response::json($subscription); + } + + public function deleteSubscription($id) + { + $subscription = Subscription::find($id); + $subscription->delete(); + + return Response::json(array("success" => true)); + } + } +?> \ No newline at end of file diff --git a/app/controllers/UserController.php b/app/controllers/UserController.php index 61ec1f9..adb569d 100644 --- a/app/controllers/UserController.php +++ b/app/controllers/UserController.php @@ -1,8 +1,8 @@ Input::get("email"), "firstname" => Input::get("firstname"), "lastname" => Input::get("lastname") - ]); + ]);*/ + + if (User::where('email','=',Input::get("email"))->count() > 0) { + //$user = User::where('email','=', Input::get("email"))->get()->first(); + return Response::json(array('success' => false , 'error' => 'User already exists with this email')); + } + + $user = new User; + $user->privilege_id = Input::get("privilege_id"); + $user->password = Input::get("password"); + $user->email = Input::get("email"); + $user->first_name = Input::get("first_name"); + $user->last_name = Input::get("last_name"); + $user->mobile_number = Input::get("mobile_number"); + $user->default_servergroup = 1;//ServerGroup::all()->first()->id; + + $user->save(); + + return Response::json($user); + } + + public function getDefaultServerGroup($id) + { + $user = User::find($id); + $serverGroup = ServerGroup::find($user->default_servergroup); + return Response::json($serverGroup); + } + + public function deleteUser($id) + { + $user = User::find($id); + $user->delete(); + + return Response::json(array("success" => true)); + } + + public function getSubscriptions($id) + { + $user = User::find($id); + $subscriptions = $user->subscription; + + return Response::json($subscriptions->each(function($subscription){ + return $subscription->serverGroup; + })); + } + + public function updateUser($id) + { + $user = User::find($id); + + $user->email = Input::get("email"); + $user->first_name = Input::get("first_name"); + $user->last_name = Input::get("last_name"); + $user->mobile_number = Input::get("mobile_number"); + + $user->save(); + + return Response::json($user); + } + + public function changePassword() + { + $user = Auth::user(); + $user->password = Hash::make(Input::get("newpassword")); + + $user->save(); + + return Response::json(array("success" => true)); } + + public function getUser($id) + { + return Response::json(User::find($id)); + } + } ?> \ No newline at end of file diff --git a/app/filters.php b/app/filters.php index 85f82c4..1008d48 100644 --- a/app/filters.php +++ b/app/filters.php @@ -44,6 +44,55 @@ return Auth::basic(); }); +Route::filter('basic.once', function() +{ + return Auth::onceBasic(); +}); + +Route::filter('superadmin.auth', function() +{ + $email = ""; + $password = ""; + + if (!isset($_SERVER['HTTP_AUTHORIZATION']) || $_SERVER['HTTP_AUTHORIZATION'] == '') + { + return Response::make('Please use basic auth to provide a username and password', 401, array('WWW-Authenticate' => 'Basic realm="your site description"')); + } + else{ + list($email, $password) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); + } + if( ! Auth::attempt(array('email' => $email, 'password' => $password))) + { + return Response::make('Username or Password Incorrect', 401, array('WWW-Authenticate' => 'Basic realm="your site description"')); + } + //if(Auth::user()->privilege_id < 2) + ////{ + /// return Response::make('You are not authorised to access this resource.', 401, array('WWW-Authenticate' => 'Basic realm="your site description"')); + //} +}); + +Route::filter('user.auth', function() +{ + $email = ""; + $password = ""; + + if (!isset($_SERVER['HTTP_AUTHORIZATION']) || $_SERVER['HTTP_AUTHORIZATION'] == '') + { + return Response::make('Please use basic auth to provide a username and password', 401, array('WWW-Authenticate' => 'Basic realm="your site description"')); + } + else{ + list($email, $password) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); + } + if( ! Auth::attempt(array('email' => $email, 'password' => $password))) + { + return Response::make('Username or Password Incorrect', 401, array('WWW-Authenticate' => 'Basic realm="your site description"')); + } + if(Auth::user()->privilege_id > 1) + { + return Response::make('You are not authorised to access this resource.', 401, array('WWW-Authenticate' => 'Basic realm="your site description"')); + } +}); + /* |-------------------------------------------------------------------------- | Guest Filter diff --git a/app/libraries/pubnub-api/3.3/Pubnub.php b/app/libraries/pubnub-api/3.3/Pubnub.php index 9109255..2f03ff8 100644 --- a/app/libraries/pubnub-api/3.3/Pubnub.php +++ b/app/libraries/pubnub-api/3.3/Pubnub.php @@ -4,504 +4,506 @@ * PubNub 3.3 Real-time Push Cloud API * @package Pubnub */ -class Pubnub -{ - private $ORIGIN = 'pubsub.pubnub.com'; - private $PUBLISH_KEY = 'demo'; - private $SUBSCRIBE_KEY = 'demo'; - private $SECRET_KEY = false; - private $CIPHER_KEY = ''; - private $SSL = false; - private $SESSION_UUID = ''; - - /** - * Pubnub - * - * Init the Pubnub Client API - * - * @param string $publish_key required key to send messages. - * @param string $subscribe_key required key to receive messages. - * @param string $secret_key optional key to sign messages. - * @param string $origin optional setting for cloud origin. - * @param boolean $ssl required for 2048 bit encrypted messages. - */ - - - function Pubnub( - $publish_key = 'demo', - $subscribe_key = 'demo', - $secret_key = false, - $cipher_key = false, - $ssl = false, - $origin = false - ) +if (!class_exists("Pubnub")){ + class Pubnub { + private $ORIGIN = 'pubsub.pubnub.com'; + private $PUBLISH_KEY = 'demo'; + private $SUBSCRIBE_KEY = 'demo'; + private $SECRET_KEY = false; + private $CIPHER_KEY = ''; + private $SSL = false; + private $SESSION_UUID = ''; + + /** + * Pubnub + * + * Init the Pubnub Client API + * + * @param string $publish_key required key to send messages. + * @param string $subscribe_key required key to receive messages. + * @param string $secret_key optional key to sign messages. + * @param string $origin optional setting for cloud origin. + * @param boolean $ssl required for 2048 bit encrypted messages. + */ + + + function Pubnub( + $publish_key = 'demo', + $subscribe_key = 'demo', + $secret_key = false, + $cipher_key = false, + $ssl = false, + $origin = false + ) + { + + $this->SESSION_UUID = $this->uuid(); + + $this->PUBLISH_KEY = $publish_key; + $this->SUBSCRIBE_KEY = $subscribe_key; + $this->SECRET_KEY = $secret_key; + + if (!isBlank($cipher_key)) { + $this->CIPHER_KEY = $cipher_key; + } - $this->SESSION_UUID = $this->uuid(); + $this->SSL = $ssl; - $this->PUBLISH_KEY = $publish_key; - $this->SUBSCRIBE_KEY = $subscribe_key; - $this->SECRET_KEY = $secret_key; + if ($origin) + $this->ORIGIN = $origin; - if (!isBlank($cipher_key)) { - $this->CIPHER_KEY = $cipher_key; + if ($ssl) + $this->ORIGIN = 'https://' . $this->ORIGIN; + else + $this->ORIGIN = 'http://' . $this->ORIGIN; } - $this->SSL = $ssl; - - if ($origin) - $this->ORIGIN = $origin; - - if ($ssl) - $this->ORIGIN = 'https://' . $this->ORIGIN; - else - $this->ORIGIN = 'http://' . $this->ORIGIN; - } + /** + * Publish + * + * Send a message to a channel. + * + * @param array $args with channel and message. + * @return array success information. + */ + + function publish($args) + { + ## Fail if bad input. + if (!(isset($args['channel']) && isset($args['message']))) { + echo('Missing Channel or Message'); + return false; + } - /** - * Publish - * - * Send a message to a channel. - * - * @param array $args with channel and message. - * @return array success information. - */ + ## Capture User Input + $channel = $args['channel']; + $message_org = $args['message']; - function publish($args) - { - ## Fail if bad input. - if (!(isset($args['channel']) && isset($args['message']))) { - echo('Missing Channel or Message'); - return false; - } + $message = $this->sendMessage($message_org); - ## Capture User Input - $channel = $args['channel']; - $message_org = $args['message']; - $message = $this->sendMessage($message_org); + ## Sign Message + $signature = "0"; + if ($this->SECRET_KEY) { + ## Generate String to Sign + $string_to_sign = implode('/', array( + $this->PUBLISH_KEY, + $this->SUBSCRIBE_KEY, + $this->SECRET_KEY, + $channel, + $message + )); + $signature = md5($string_to_sign); + } - ## Sign Message - $signature = "0"; - if ($this->SECRET_KEY) { - ## Generate String to Sign - $string_to_sign = implode('/', array( + ## Send Message + $publishResponse = $this->_request(array( + 'publish', $this->PUBLISH_KEY, $this->SUBSCRIBE_KEY, - $this->SECRET_KEY, + $signature, $channel, + '0', $message )); - $signature = md5($string_to_sign); - } - - ## Send Message - $publishResponse = $this->_request(array( - 'publish', - $this->PUBLISH_KEY, - $this->SUBSCRIBE_KEY, - $signature, - $channel, - '0', - $message - )); - - if ($publishResponse == null) - return array(0, "Error during publish."); - else - return $publishResponse; + if ($publishResponse == null) + return array(0, "Error during publish."); + else + return $publishResponse; - } + } - public function sendMessage($message_org) + public function sendMessage($message_org) - { - if ($this->CIPHER_KEY != false) { - $message = json_encode(encrypt(json_encode($message_org), $this->CIPHER_KEY)); - } else { - $message = json_encode($message_org); + { + if ($this->CIPHER_KEY != false) { + $message = json_encode(encrypt(json_encode($message_org), $this->CIPHER_KEY)); + } else { + $message = json_encode($message_org); + } + return $message; } - return $message; - } - function here_now($args) - { - if (!($args['channel'])) { - echo('Missing Channel'); - return false; - } + function here_now($args) + { + if (!($args['channel'])) { + echo('Missing Channel'); + return false; + } - ## Capture User Input - $channel = $args['channel']; - - return $this->_request(array( - 'v2', - 'presence', - 'sub_key', - $this->SUBSCRIBE_KEY, - 'channel', - $channel - )); - } + ## Capture User Input + $channel = $args['channel']; - /** - * Subscribe - * - * This is BLOCKING. - * Listen for a message on a channel. - * - * @param array $args with channel and message. - * @return mixed false on fail, array on success. - */ - function subscribe($args, $presence = false) - { - ## Capture User Input - $channel = $args['channel']; - $callback = $args['callback']; - $timetoken = isset($args['timetoken']) ? $args['timetoken'] : '0'; - - ## Fail if missing channel - if (!$channel) { - echo("Missing Channel.\n"); - return false; + return $this->_request(array( + 'v2', + 'presence', + 'sub_key', + $this->SUBSCRIBE_KEY, + 'channel', + $channel + )); } - ## Fail if missing callback - if (!$callback) { - echo("Missing Callback.\n"); - return false; - } + /** + * Subscribe + * + * This is BLOCKING. + * Listen for a message on a channel. + * + * @param array $args with channel and message. + * @return mixed false on fail, array on success. + */ + function subscribe($args, $presence = false) + { + ## Capture User Input + $channel = $args['channel']; + $callback = $args['callback']; + $timetoken = isset($args['timetoken']) ? $args['timetoken'] : '0'; + + ## Fail if missing channel + if (!$channel) { + echo("Missing Channel.\n"); + return false; + } - if ($presence == true) { - $mode = "presence"; - } else - $mode = "default"; + ## Fail if missing callback + if (!$callback) { + echo("Missing Callback.\n"); + return false; + } + if ($presence == true) { + $mode = "presence"; + } else + $mode = "default"; - while (1) { - try { - ## Wait for Message - $response = $this->_request(array( - 'subscribe', - $this->SUBSCRIBE_KEY, - $channel, - '0', - $timetoken - )); + while (1) { - if ($response == "_PUBNUB_TIMEOUT") { - continue; - } elseif ($response == "_PUBNUB_MESSAGE_TOO_LARGE") { - $timetoken = $this->throwAndResetTimetoken($callback, "Message Too Large"); - continue; - } elseif ($response == null || $timetoken == null) { - $timetoken = $this->throwAndResetTimetoken($callback, "Bad server response."); - continue; - } + try { + ## Wait for Message + $response = $this->_request(array( + 'subscribe', + $this->SUBSCRIBE_KEY, + $channel, + '0', + $timetoken + )); - $messages = $response[0]; - $timetoken = $response[1]; + if ($response == "_PUBNUB_TIMEOUT") { + continue; + } elseif ($response == "_PUBNUB_MESSAGE_TOO_LARGE") { + $timetoken = $this->throwAndResetTimetoken($callback, "Message Too Large"); + continue; + } elseif ($response == null || $timetoken == null) { + $timetoken = $this->throwAndResetTimetoken($callback, "Bad server response."); + continue; + } - if (!count($messages)) { - continue; - } + $messages = $response[0]; + $timetoken = $response[1]; + + if (!count($messages)) { + continue; + } - $receivedMessages = $this->decodeAndDecrypt($messages, $mode); + $receivedMessages = $this->decodeAndDecrypt($messages, $mode); - $returnArray = array($receivedMessages, $timetoken); + $returnArray = array($receivedMessages, $timetoken); - $callback($returnArray); + $callback($returnArray); - } catch (Exception $error) { - $this->handleError($error, $args); - $timetoken = $this->throwAndResetTimetoken($callback, "Unknown error."); - continue; + } catch (Exception $error) { + $this->handleError($error, $args); + $timetoken = $this->throwAndResetTimetoken($callback, "Unknown error."); + continue; + } } } - } - - public function throwAndResetTimetoken($callback, $errorMessage) - { - $callback(array(0, $errorMessage)); - $timetoken = "0"; - return $timetoken; - } - - public function decodeAndDecrypt($messages, $mode = "default") - { - $receivedMessages = array(); - if ($mode == "presence") { - return $messages; - - } elseif ($mode == "default") { - $messageArray = $messages; - $receivedMessages = $this->decodeDecryptLoop($messageArray); + public function throwAndResetTimetoken($callback, $errorMessage) + { + $callback(array(0, $errorMessage)); + $timetoken = "0"; + return $timetoken; + } - } elseif ($mode == "detailedHistory") { + public function decodeAndDecrypt($messages, $mode = "default") + { + $receivedMessages = array(); - $messageArray = $messages[0]; - $receivedMessages = $this->decodeDecryptLoop($messageArray); - } + if ($mode == "presence") { + return $messages; - return $receivedMessages; - } + } elseif ($mode == "default") { + $messageArray = $messages; + $receivedMessages = $this->decodeDecryptLoop($messageArray); - public function decodeDecryptLoop($messageArray) - { - $receivedMessages = array(); - foreach ($messageArray as $message) { + } elseif ($mode == "detailedHistory") { - if ($this->CIPHER_KEY) { - $decryptedMessage = decrypt($message, $this->CIPHER_KEY); - $message = json_decode($decryptedMessage, true); + $messageArray = $messages[0]; + $receivedMessages = $this->decodeDecryptLoop($messageArray); } - array_push($receivedMessages, $message); + return $receivedMessages; } - return $receivedMessages; - } + public function decodeDecryptLoop($messageArray) + { + $receivedMessages = array(); + foreach ($messageArray as $message) { - public function handleError($error, $args) - { - $errorMsg = 'Error on line ' . $error->getLine() . ' in ' . $error->getFile() . $error->getMessage(); - trigger_error($errorMsg, E_COMPILE_WARNING); + if ($this->CIPHER_KEY) { + $decryptedMessage = decrypt($message, $this->CIPHER_KEY); + $message = json_decode($decryptedMessage, true); + } + array_push($receivedMessages, $message); + } + return $receivedMessages; + } - sleep(1); - } - /** - * Presence - * - * This is BLOCKING. - * Listen for a message on a channel. - * - * @param array $args with channel and message. - * @return mixed false on fail, array on success. - */ - function presence($args) - { - ## Capture User Input - $args['channel'] = ($args['channel'] . "-pnpres"); - $this->subscribe($args, true); - } + public function handleError($error, $args) + { + $errorMsg = 'Error on line ' . $error->getLine() . ' in ' . $error->getFile() . $error->getMessage(); + trigger_error($errorMsg, E_COMPILE_WARNING); - /** - * Detailed History - * - * Load history from a channel. - * - * @param array $args with 'channel' and 'limit'. - * @return mixed false on fail, array on success. - */ - function detailedHistory($args) - { - ## Capture User Input + sleep(1); + } - ## Fail if bad input. - if (!$args['channel']) { - echo('Missing Channel'); - return false; + /** + * Presence + * + * This is BLOCKING. + * Listen for a message on a channel. + * + * @param array $args with channel and message. + * @return mixed false on fail, array on success. + */ + function presence($args) + { + ## Capture User Input + $args['channel'] = ($args['channel'] . "-pnpres"); + $this->subscribe($args, true); } - $channel = $args['channel']; - $urlParams = ""; + /** + * Detailed History + * + * Load history from a channel. + * + * @param array $args with 'channel' and 'limit'. + * @return mixed false on fail, array on success. + */ + + function detailedHistory($args) + { + ## Capture User Input + + ## Fail if bad input. + if (!$args['channel']) { + echo('Missing Channel'); + return false; + } - if ($args['count'] || $args['start'] || $args['end'] || $args['reverse']) { + $channel = $args['channel']; + $urlParams = ""; - $urlParamSep = "?"; - if (isset($args['count'])) { - $urlParams .= $urlParamSep . "count=" . $args['count']; - $urlParamSep = "&"; - } - if (isset($args['start'])) { - $urlParams .= $urlParamSep . "start=" . $args['start']; - $urlParamSep = "&"; - } - if (isset($args['end'])) { - $urlParams .= $urlParamSep . "end=" . $args['end']; - $urlParamSep = "&"; - } - if (isset($args['reverse'])) { - $urlParams .= $urlParamSep . "reverse=" . $args['reverse']; - } + if ($args['count'] || $args['start'] || $args['end'] || $args['reverse']) { - } + $urlParamSep = "?"; + if (isset($args['count'])) { + $urlParams .= $urlParamSep . "count=" . $args['count']; + $urlParamSep = "&"; + } + if (isset($args['start'])) { + $urlParams .= $urlParamSep . "start=" . $args['start']; + $urlParamSep = "&"; + } + if (isset($args['end'])) { + $urlParams .= $urlParamSep . "end=" . $args['end']; + $urlParamSep = "&"; + } + if (isset($args['reverse'])) { + $urlParams .= $urlParamSep . "reverse=" . $args['reverse']; + } - $response = $this->_request(array( - 'v2', - 'history', - "sub-key", - $this->SUBSCRIBE_KEY, - "channel", - $channel - ), $urlParams); - ; + } - $receivedMessages = $this->decodeAndDecrypt($response, "detailedHistory"); + $response = $this->_request(array( + 'v2', + 'history', + "sub-key", + $this->SUBSCRIBE_KEY, + "channel", + $channel + ), $urlParams); + ; - return $receivedMessages; + $receivedMessages = $this->decodeAndDecrypt($response, "detailedHistory"); - } + return $receivedMessages; - /** - * History - * - * Load history from a channel. - * - * @param array $args with 'channel' and 'limit'. - * @return mixed false on fail, array on success. - */ - function history($args) - { - ## Capture User Input - $limit = +$args['limit'] ? +$args['limit'] : 10; - $channel = $args['channel']; - - ## Fail if bad input. - if (!$channel) { - echo('Missing Channel'); - return false; } - ## Get History - $response = $this->_request(array( - 'history', - $this->SUBSCRIBE_KEY, - $channel, - '0', - $limit - )); - ; + /** + * History + * + * Load history from a channel. + * + * @param array $args with 'channel' and 'limit'. + * @return mixed false on fail, array on success. + */ + function history($args) + { + ## Capture User Input + $limit = +$args['limit'] ? +$args['limit'] : 10; + $channel = $args['channel']; + + ## Fail if bad input. + if (!$channel) { + echo('Missing Channel'); + return false; + } - $receivedMessages = $this->decodeAndDecrypt($response); + ## Get History + $response = $this->_request(array( + 'history', + $this->SUBSCRIBE_KEY, + $channel, + '0', + $limit + )); + ; - return $receivedMessages; + $receivedMessages = $this->decodeAndDecrypt($response); - } + return $receivedMessages; - /** - * Time - * - * Timestamp from PubNub Cloud. - * - * @return int timestamp. - */ - function time() - { - ## Get History - $response = $this->_request(array( - 'time', - '0' - )); + } - return $response[0]; - } + /** + * Time + * + * Timestamp from PubNub Cloud. + * + * @return int timestamp. + */ + function time() + { + ## Get History + $response = $this->_request(array( + 'time', + '0' + )); - /** - * UUID - * - * UUID generator - * - * @return UUID - */ - function uuid() - { - if (function_exists('com_create_guid') === true) { - return trim(com_create_guid(), '{}'); + return $response[0]; } - return sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535)); - } + /** + * UUID + * + * UUID generator + * + * @return UUID + */ + function uuid() + { + if (function_exists('com_create_guid') === true) { + return trim(com_create_guid(), '{}'); + } - /** - * Request URL - * - * @param array $request of url directories. - * @return array from JSON response. - */ - private function _request($request, $urlParams = false) - { - $request = array_map('Pubnub::_encode', $request); + return sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535)); + } - array_unshift($request, $this->ORIGIN); + /** + * Request URL + * + * @param array $request of url directories. + * @return array from JSON response. + */ + private function _request($request, $urlParams = false) + { + $request = array_map('Pubnub::_encode', $request); - if (($request[1] === 'presence') || ($request[1] === 'subscribe')) { - array_push($request, '?uuid=' . $this->SESSION_UUID); - } + array_unshift($request, $this->ORIGIN); - $urlString = implode('/', $request); + if (($request[1] === 'presence') || ($request[1] === 'subscribe')) { + array_push($request, '?uuid=' . $this->SESSION_UUID); + } - if ($urlParams) { - $urlString .= $urlParams; - } + $urlString = implode('/', $request); - $ch = curl_init(); + if ($urlParams) { + $urlString .= $urlParams; + } - $pubnubHeaders = array("V: 3.3", "Accept: */*"); - curl_setopt($ch, CURLOPT_HTTPHEADER, $pubnubHeaders); - curl_setopt($ch, CURLOPT_USERAGENT, "PHP"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_TIMEOUT, 300); + $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $urlString); + $pubnubHeaders = array("V: 3.3", "Accept: */*"); + curl_setopt($ch, CURLOPT_HTTPHEADER, $pubnubHeaders); + curl_setopt($ch, CURLOPT_USERAGENT, "PHP"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 300); - if ($this->SSL) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_CAINFO, getcwd() . "/pubnub.com.pem"); - } + curl_setopt($ch, CURLOPT_URL, $urlString); - $output = curl_exec($ch); - $curlError = curl_errno($ch); - $curlResponseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($this->SSL) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_CAINFO, getcwd() . "/pubnub.com.pem"); + } - curl_close($ch); + $output = curl_exec($ch); + $curlError = curl_errno($ch); + $curlResponseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $JSONdecodedResponse = json_decode($output, true); + curl_close($ch); - if ($JSONdecodedResponse != null) - return $JSONdecodedResponse; - elseif ($curlError == 28) - return "_PUBNUB_TIMEOUT"; - elseif ($curlResponseCode == 400 || $curlResponseCode == 404) - return "_PUBNUB_MESSAGE_TOO_LARGE"; + $JSONdecodedResponse = json_decode($output, true); - } + if ($JSONdecodedResponse != null) + return $JSONdecodedResponse; + elseif ($curlError == 28) + return "_PUBNUB_TIMEOUT"; + elseif ($curlResponseCode == 400 || $curlResponseCode == 404) + return "_PUBNUB_MESSAGE_TOO_LARGE"; - /** - * Encode - * - * @param string $part of url directories. - * @return string encoded string. - */ - private static function _encode($part) - { - $pieces = array_map('Pubnub::_encode_char', str_split($part)); - return implode('', $pieces); - } + } - /** - * Encode Char - * - * @param string $char val. - * @return string encoded char. - */ - private static function _encode_char($char) - { - if (strpos(' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?', $char) === false) - return $char; - else - return rawurlencode($char); + /** + * Encode + * + * @param string $part of url directories. + * @return string encoded string. + */ + private static function _encode($part) + { + $pieces = array_map('Pubnub::_encode_char', str_split($part)); + return implode('', $pieces); + } + + /** + * Encode Char + * + * @param string $char val. + * @return string encoded char. + */ + private static function _encode_char($char) + { + if (strpos(' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?', $char) === false) + return $char; + else + return rawurlencode($char); + } } } diff --git a/app/models/Privilege.php b/app/models/Privilege.php new file mode 100644 index 0000000..eacfb4c --- /dev/null +++ b/app/models/Privilege.php @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/app/models/Pulse.php b/app/models/Pulse.php index e70752f..57eb562 100644 --- a/app/models/Pulse.php +++ b/app/models/Pulse.php @@ -20,4 +20,16 @@ public function store() { // } + + public function scopeOfServerID($query, $serverID) + { + return $query->where('server_id', '=', $serverID); + } + + public function scopeOfDays($query, $days) + { + $daysAsSeconds = $days * 86400; + + return $query->where('timestamp', '>', time() - $daysAsSeconds); + } } \ No newline at end of file diff --git a/app/models/Server.php b/app/models/Server.php new file mode 100644 index 0000000..5eb9f71 --- /dev/null +++ b/app/models/Server.php @@ -0,0 +1,43 @@ +hasMany('Pulse', 'server_id', 'id'); + } + + public function scopeOfUnassigned($query, $status) + { + return $query->where('servergroup_id', '=', $status); + } + + public function scopeofAssigned($query, $status) + { + return $query->where('servergroup_id', '>', $status); + } + + public function scopeOfStatus($query, $status) + { + return $query->where('online', '=', $status); + } + } +?> \ No newline at end of file diff --git a/app/models/ServerGroup.php b/app/models/ServerGroup.php new file mode 100644 index 0000000..c6cf746 --- /dev/null +++ b/app/models/ServerGroup.php @@ -0,0 +1,27 @@ +hasMany('Server', 'servergroup_id', 'id'); + } + } +?> \ No newline at end of file diff --git a/app/models/Subscription.php b/app/models/Subscription.php new file mode 100644 index 0000000..12ed8bb --- /dev/null +++ b/app/models/Subscription.php @@ -0,0 +1,51 @@ +delete(); + + return Response::json(array("success" => true)); + } + + public function serverGroup() + { + return $this->hasOne('ServerGroup', 'id', 'servergroup_id'); + } + + public function user() + { + return $this->hasOne('User', 'id', 'user_id'); + } + + public function scopeOfUser($query, $user_id) + { + return $query->where('user_id', '=', $user_id); + } + + public function scopeOfServerID($query, $id) + { + return $query->where('servergroup_id', '=', $id); + } + } +?> \ No newline at end of file diff --git a/app/models/User.php b/app/models/User.php index 342f1b3..98dc894 100644 --- a/app/models/User.php +++ b/app/models/User.php @@ -13,7 +13,7 @@ class User extends Eloquent implements UserInterface, RemindableInterface { protected $hidden = array('password'); protected $fillable = array('email', 'first_name', 'last_name', 'mobile_number'); - protected $guarded = array('id', 'password', 'password_salt', 'privilege_id'); + protected $guarded = array('id', 'password', 'password_salt', 'privilege_id', 'locked_out', 'attempts', 'default_servergroup'); public $timestamps = false; /** @@ -55,4 +55,28 @@ public function store() { // } + + public function getRememberToken() + { + return $this->remember_token; + } + + public function setRememberToken($value) + { + $this->remember_token = $value; + } + + public function getRememberTokenName() + { + return 'remember_token'; + } + + public function setPasswordAttribute($pass){ + $this->attributes['password'] = Hash::make($pass); + } + + public function subscription() + { + return $this->hasMany('Subscription', 'user_id', 'id'); + } } \ No newline at end of file diff --git a/app/routes.php b/app/routes.php index 3286639..cd629ac 100644 --- a/app/routes.php +++ b/app/routes.php @@ -11,22 +11,59 @@ | */ -Route::get('/', function() -{ - return "Hello World"; -}); +//Auth route +Route::get('/auth', 'AuthenticationController@auth'); -Route::get('/jimmy', function() -{ - return Response::json("Jimmy"); -}); +Route::group(array('before' => 'superadmin.auth'), function() { -Route::get('/users', 'UserController@getAll'); -Route::get('/users/store', 'UserController@store'); + //User routes + Route::get('/users', 'UserController@getAll'); + Route::get('/users/{id}', 'UserController@getUser'); + Route::get('/users/{id}/servergroups/default', 'UserController@getDefaultServerGroup'); + Route::post('/users', 'UserController@store'); + Route::delete('/users/{id}', 'UserController@deleteUser'); + Route::get('/users/{id}/subscriptions', 'UserController@getSubscriptions'); + Route::put('/users/{id}', 'UserController@updateUser'); + Route::put('/users/password', 'UserController@changePassword'); + -Route::get('/pulse', 'PulseController@getAll'); -Route::post('/pulse', 'PulseController@store'); + //Pulse routes + Route::get('/pulses', 'PulseController@getAll'); + Route::post('/pulses', 'PulseController@store'); + Route::delete('/pulses/{id}', 'PulseController@deletePulse'); -Route::any('/pubnub', 'pubnub::simplechat@index'); -Route::any('(:bundle)/login', 'pubnub::simplechat@login'); -Route::any('(:bundle)/logout', 'pubnub::simplechat@logout'); \ No newline at end of file + //Server routes + Route::get('/servers/unassigned', 'ServerController@getUnassignedServers'); + Route::get('/servers/assigned', 'ServerController@getAssignedServers'); + Route::put('/servers/{id}/servergroup', 'ServerController@updateServerGroup'); + Route::get('/servers/status/{status}', 'ServerController@getServersForStatus'); + Route::get('/servers/{id}', 'ServerController@getServer'); + Route::put('/servers/{guid}', 'ServerController@updateServerDetails'); + Route::delete('/servers/{id}', 'ServerController@deleteServer'); + Route::get('/servers', 'ServerController@getAll'); + Route::post('/servers', 'ServerController@store'); + Route::put('/servers/{guid}/status', 'ServerController@changeStatus'); + Route::get('/servers/{guid}/status', 'ServerController@getStatus'); + Route::get('/servers/{id}/pulses', 'ServerController@getPulses'); + Route::get('/servers/{id}/pulses/latest', 'ServerController@getLatestPulse'); + Route::get('/servers/{id}/pulses/{days}', 'ServerController@getPulsesForDays'); + + //Server Group routes + Route::get('/servergroups', 'ServerGroupController@getAll'); + Route::get('/servergroups/{id}', 'ServerGroupController@getServerGroup'); + Route::post('/servergroups', 'ServerGroupController@store'); + Route::put('/servergroups/{id}', 'ServerGroupController@editServerGroup'); + Route::delete('/servergroups/{id}', 'ServerGroupController@deleteServerGroup'); + Route::get('/servergroups/{id}/servers', 'ServerGroupController@getServers'); + + //Privileges routes + Route::get('/privileges', 'PrivilegeController@getAll'); + Route::post('/privileges', 'PrivilegeController@store'); + Route::delete('/privileges/{id}', 'PrivilegeController@deletePrivilege'); + + //Subscription routes + Route::get('/subscriptions', 'SubscriptionController@getAll'); + Route::post('/subscriptions', 'SubscriptionController@store'); + Route::put('/subscriptions/{id}', 'SubscriptionController@updateSubscription'); + Route::delete('/subscriptions/{id}', 'SubscriptionController@deleteSubscription'); +}); \ No newline at end of file diff --git a/app/tests/ExampleTest.php b/app/tests/ExampleTest.php deleted file mode 100644 index ead53e0..0000000 --- a/app/tests/ExampleTest.php +++ /dev/null @@ -1,17 +0,0 @@ -client->request('GET', '/'); - - $this->assertTrue($this->client->getResponse()->isOk()); - } - -} \ No newline at end of file diff --git a/app/tests/PrivilegeTest.php b/app/tests/PrivilegeTest.php new file mode 100644 index 0000000..7e6a9ca --- /dev/null +++ b/app/tests/PrivilegeTest.php @@ -0,0 +1,24 @@ +call('POST', 'privileges', array("name" => "TestPrivilege")); + $responseAsJson = json_decode($response->getContent()); + + $this->assertEquals("TestPrivilege", $responseAsJson->name); + + return $responseAsJson->id; + } + + /** + * @depends testNewAdd + */ + public function testDelete($id) + { + $response = $this->call("DELETE", "privileges/$id"); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(true, $responseAsJson->success); + } + } +?> \ No newline at end of file diff --git a/app/tests/PulseTest.php b/app/tests/PulseTest.php new file mode 100644 index 0000000..6606bf3 --- /dev/null +++ b/app/tests/PulseTest.php @@ -0,0 +1,24 @@ +call('POST', 'pulses', array("server_id" => 1, "ram_usage" => 500, "cpu_usage" => 500, "disk_usage" => 500, "uptime" => 500, "timestamp" => 500)); + $responseAsJson = json_decode($response->getContent()); + + $this->assertEquals(500, $responseAsJson->cpu_usage); + + return $responseAsJson->id; + } + + /** + * @depends testNewAdd + */ + public function testDelete($id) + { + $response = $this->call("DELETE", "pulses/$id"); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(true, $responseAsJson->success); + } + } +?> \ No newline at end of file diff --git a/app/tests/ServerGroupTest.php b/app/tests/ServerGroupTest.php new file mode 100644 index 0000000..88aca78 --- /dev/null +++ b/app/tests/ServerGroupTest.php @@ -0,0 +1,43 @@ +call('POST', 'servergroups', array('name' => "Test ServerGroup")); + $responseAsJson = json_decode($response->getContent()); + + $this->assertEquals("Test ServerGroup", $responseAsJson->name); + + return $responseAsJson->id; + } + + public function testExistingAdd() + { + $response = $this->call('POST', 'servergroups', array('name' => "Test ServerGroup")); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(false, $responseAsJson->success); + } + + /** + * @depends testNewAdd + */ + public function testEdit($id) + { + $response = $this->call("PUT", "servergroups/$id", array('name' => 'Test ServerGroupEdit')); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals("Test ServerGroupEdit", $responseAsJson->name); + } + + /** + * @depends testNewAdd + */ + public function testDelete($id) + { + $response = $this->call("DELETE", "servergroups/$id"); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(true, $responseAsJson->success); + } + + + } +?> \ No newline at end of file diff --git a/app/tests/ServerTest.php b/app/tests/ServerTest.php new file mode 100644 index 0000000..1988a90 --- /dev/null +++ b/app/tests/ServerTest.php @@ -0,0 +1,24 @@ +call('POST', 'servers', array('servergroup_id' => 0, 'name' => "Test Server", 'available_disk' => 500, 'available_ram' => 500, 'cpu_speed' => 500, 'os_name' => "Test OS", 'os_version'=> "Test Version", "guid" => "NEWGUID")); + $responseAsJson = json_decode($response->getContent()); + + $this->assertEquals("Test Server", $responseAsJson->name); + + return $responseAsJson->id; + } + + /** + * @depends testNewAdd + */ + public function testDelete($id) + { + $response = $this->call("DELETE", "servers/$id"); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(true, $responseAsJson->success); + } + } +?> \ No newline at end of file diff --git a/app/tests/SubscriptionTest.php b/app/tests/SubscriptionTest.php new file mode 100644 index 0000000..3d7c455 --- /dev/null +++ b/app/tests/SubscriptionTest.php @@ -0,0 +1,24 @@ +call('POST', 'subscriptions', array('servergroup_id' => 33, 'user_id' => 1, 'phonecall' => 1, 'text' => 1, 'push' => 1)); + $responseAsJson = json_decode($response->getContent()); + + $this->assertEquals(33, $responseAsJson->servergroup_id); + + return $responseAsJson->id; + } + + /** + * @depends testNewAdd + */ + public function testDelete($id) + { + $response = $this->call("DELETE", "subscriptions/$id"); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(true, $responseAsJson->success); + } + } +?> \ No newline at end of file diff --git a/app/tests/UserTest.php b/app/tests/UserTest.php new file mode 100644 index 0000000..f911459 --- /dev/null +++ b/app/tests/UserTest.php @@ -0,0 +1,41 @@ +call('POST', 'users', array('privilege_id' => 3, 'password' => 'Test', 'email' => "testing@testing.com", 'first_name' => 'Test' , 'last_name' => 'Test', 'mobile_number' => '+447935928168')); + $responseAsJson = json_decode($response->getContent()); + + $this->assertEquals(3, $responseAsJson->privilege_id); + + return $responseAsJson->id; + } + + public function testExistingAdd() + { + $response = $this->call('POST', 'users', array('privilege_id' => 3, 'password' => 'Test', 'email' => "testing@testing.com", 'first_name' => 'Test' , 'last_name' => 'Test', 'mobile_number' => '+447935928168')); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(false, $responseAsJson->success); + } + + /** + * @depends testNewAdd + */ + public function testEdit($id) + { + $response = $this->call('PUT', "users/$id", array('email' => "testing@testing.com", 'first_name' => 'Test' , 'last_name' => 'Test', 'mobile_number' => '+447935928168')); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals("testing@testing.com", $responseAsJson->email); + } + + /** + * @depends testNewAdd + */ + public function testDelete($id) + { + $response = $this->call("DELETE", "users/$id"); + $responseAsJson = json_decode($response->getContent()); + $this->assertEquals(true, $responseAsJson->success); + } + } +?> \ No newline at end of file diff --git a/composer.json b/composer.json index bb5662a..6d3dc4f 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "keywords": ["framework", "laravel"], "license": "MIT", "require": { - "laravel/framework": "4.1.*" + "laravel/framework": "4.1.*", + "travisjryan/twilio": "dev-master" }, "autoload": { "classmap": [ @@ -32,5 +33,5 @@ "config": { "preferred-install": "dist" }, - "minimum-stability": "stable" -} + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index c42dc4f..149652a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,7 +2,7 @@ + + + + ./app/tests/ diff --git a/voicealert.xml b/voicealert.xml new file mode 100644 index 0000000..dcdf6cf --- /dev/null +++ b/voicealert.xml @@ -0,0 +1,3 @@ + +Hey, your server has gone offline. Please attend to it. + \ No newline at end of file diff --git a/vpu/.gitignore b/vpu/.gitignore new file mode 100644 index 0000000..e934adf --- /dev/null +++ b/vpu/.gitignore @@ -0,0 +1 @@ +cache/ diff --git a/vpu/.htaccess b/vpu/.htaccess new file mode 100644 index 0000000..447ec9a --- /dev/null +++ b/vpu/.htaccess @@ -0,0 +1,6 @@ + + RewriteEngine on + RewriteBase /vpu + RewriteRule ^$ app/public/ [L] + RewriteRule (.*) app/public/$1 [L] + diff --git a/vpu/CHANGELOG.md b/vpu/CHANGELOG.md new file mode 100644 index 0000000..c40a13b --- /dev/null +++ b/vpu/CHANGELOG.md @@ -0,0 +1,130 @@ +# Changelog + +## v2.2 + +* Add CLI switches to allow config overrides (GH-93) +* Add support for multiple XML files (GH-93) +* Allow for multiple test directories to be specified (GH-93) +* Throw exception if cache permissions are incorrect (GH-96) + +## v2.1.1 + +* Don't allow duplicate files if the parent folder is selected (GH-90) +* Pad snapshot time with zero for better sorting (GH-87) +* Only collect JSON when using XML configuration files (GH-84) +* Show server error if AJAX request fails (GH-83) +* Don't rewrite PHP_SELF (GH-80) +* Use namespace when checking if tests are subclasses of PHPUnit_Framework_TestCase (GH-78) +* Fix WAMP routing issues (GH-53) +* Implement keyboard shortcuts (GH-67) +* Fix output parsing to handle pretty-printed JSON (GH-65) +* Fix display of statistics (GH-63) +* Clarify directory selection key combination (GH-60) +* Check if tests are subclasses of PHPUnit_Framework_TestCase (GH-59) + +## v2.1 + +* Add ability to ignore hidden files (GH-57) +* Add error handler for non-JSON responses from the server (GH-48, GH-58) +* Use strict checking with readdir() (GH-56) +* Handle unbalanced braces properly (GH-54) +* Fix error that occurs when no snapshot is selected on Archives page (GH-50) +* Reduce complexity of Apache installations (GH-45) +* Fix autoloader to only load files required by VPU (GH-46) +* Only return child directories of test_directory (GH-44) + +## v2.0 + +* Overhaul the entire code base +* Give the UI a facelift +* Add ability to run tests using a phpunit.xml configuration file (GH-31) +* Add ability to generate test results from the command line (GH-32) + +## v1.5.6 + +* Replace line breaks with
s instead of empty strings (GH-42) +* Fix jqueryFileTree folder selection for Macs (GH-41) +* Fix display of debugging output (GH-39) +* Add ability to set MySQL port (GH-37) + +## v1.5.5 + +* Change require -> require_once to avoid errors (GH-34) +* Don't require files to share the same name as the test classes (GH-33) +* Fix output buffering (GH-23) + +## v1.5.4 + +* Fix include_path issues (GH-26) + +## v1.5.3 + +* Fix SANDBOX_IGNORE settings (GH-21) +* Update history file (GH-20) + +## v1.5.2 + +* Add tooltips to compensate for colorblind usage problem (GH-17) +* Add ability to filter suite results (GH-14) + +## v1.5.1 + +* Update color scheme +* Update snapshot list each time a test is run (GH-10) +* Fix snapshot filenames to be compatible with Windows systems (GH-11) +* Allow debug display of JSON within tests (GH-9) +* Fix POST locations to use relative URIs + +## v1.5 + +* Add ability to generate graphs of test results + +## v1.4.1 + +* Fix Windows path issues +* Add a progress bar to indicate that tests are being processed + +## v1.4 + +* Overhaul the UI +* Fix issues with namespaced tests +* Implement a better check for archived files + + +## v1.3.2 + +* Add support for bootstraps +* Clean up the user interface +* Add the ability to view snapshots from the homepage +* Change the snapshot filename format to Y-m-d + +## v1.3.1 + +* Allow for relative paths in TEST_DIRECTORY +* Use a better test loading mechanism + +## v1.3 + +* Add a start page to allow for specific choosing of tests and options +* Add the ability to sort suite results by status and time +* Clean up some configuration settings +* Remove ability to save JSON snapshots + +## v1.2 + +* Add statistic bars to display the suite results visually + +## v1.1.1 + +* Fix to allow for loading a single test directly +* Adjust code to allow for proper execution with 'short_open_tag' off +* Fix to match test files with the word 'Test' at the end of the filename +* Fix to eliminate duplicate tests + +## v1.1 + +* Add suite statistics + +## v1.0 + +* Initial release diff --git a/vpu/LICENSE b/vpu/LICENSE new file mode 100644 index 0000000..b651ea6 --- /dev/null +++ b/vpu/LICENSE @@ -0,0 +1,33 @@ +VisualPHPUnit + +Copyright (c) 2011-2012, Nick Sinopoli . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Nick Sinopoli nor the names of his + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/vpu/README.md b/vpu/README.md new file mode 100644 index 0000000..8e2ce5d --- /dev/null +++ b/vpu/README.md @@ -0,0 +1,189 @@ +# VisualPHPUnit + +VisualPHPUnit is a visual front-end for PHPUnit. It offers the following features: + +* A stunning front-end which organizes test and suite results +* The ability to view unit testing progress via graphs +* An option to maintain a history of unit test results through the use of snapshots +* Enumeration of PHPUnit statistics and messages +* Convenient display of any debug messages written within unit tests +* Sandboxing of PHP errors +* The ability to generate test results from both a browser and the command line + +## Screenshots + +![Screenshot of VisualPHPUnit, displaying a breakdown of test results.](http://nsinopoli.github.com/VisualPHPUnit/vpu2_main.png "VisualPHPUnit Test Results") +![Screenshot of VisualPHPUnit, displaying a graph of test results.](http://nsinopoli.github.com/VisualPHPUnit/vpu2_graphs.png "VisualPHPUnit Statistics Graph") + +## Requirements + +VisualPHPUnit requires PHP 5.3+ and PHPUnit v3.5+. + +## Upgrading From v1.x + +VPU underwent a complete rewrite in v2.0. Users who are looking to upgrade from v1.x are encouraged to follow the installation instructions outlined below. + +### What About My Data? + +Because the UI has been changed, snapshots from v1.x will not render correctly in v2.x. + +Test statistics generated in v1.x, however, can still be used. When installing, ignore the [migration](#graph-generation) and run the following commands against your old VPU database instead: + +```sql +alter table SuiteResult change success succeeded int(11) not null; +alter table TestResult change success succeeded int(11) not null; +``` + +### I Miss v1.x! + +While no longer actively supported, v1.x can be found on its [own branch](https://github.com/NSinopoli/VisualPHPUnit/tree/1.x). + +## Installation + +1. Download and extract (or git clone) the project to a web-accessible directory. +2. Change the permissions of `app/resource/cache` to `777`. +3. Open `app/config/bootstrap.php` with your favorite editor. + 1. Within the `$config` array, change `pear_path` so that it points to the directory where PEAR is located. + 2. Within the `$config` array, change the contents of `test_directories` to reflect the location(s) of your unit tests. Note that each directory acts as a root directory. +4. Configure your web server (see below). + +## Web Server Configuration + +### Apache + +VPU comes with .htaccess files, so you won't have to worry about configuring anything. Simply point your browser at the location where you installed the code! + +#### Troubleshooting + +1. Make sure `mod_rewrite` is enabled. +2. Make sure `AllowOverride` in your `httpd.conf` is set to `all`. +3. If you're using WAMP, you'll need to adjust the two `.htaccess` files to reflect the location where you extracted VPU. (In this example, VPU has been extracted to `C:\wamp\www\vpu`, where `C:/wamp/www/` has been set as the `DocumentRoot` in `httpd.conf`.) + - In the `.htaccess` file located at the root of the repository, add the following line after line 2: + `RewriteBase /vpu` + - In `app/public/.htaccess`, add the following line after line 2: + `RewriteBase /vpu/app/public` + +### nginx + +Place this code block within the `http {}` block in your `nginx.conf` file: + +```nginx + + server { + server_name vpu; + root /srv/http/vpu/app/public; + index index.php; + + access_log /var/log/nginx/vpu_access.log; + error_log /var/log/nginx/vpu_error.log; + + location / { + try_files $uri /index.php; + } + + location ~ \.php$ { + fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + } +``` + +Note that you will have to change the `server_name` to the name you use in your hosts file. You will also have to adjust the directories according to where you installed the code. In this configuration, /srv/http/vpu/ is the project root. The public-facing part of VisualPHPUnit, however, is located in app/public within the project root (so in this example, it's /srv/http/vpu/app/public). + +When that's done, restart your web server, and then point your browser at the server name you chose above! + +## Project Configuration (optional) + +VPU comes with many of its features disabled by default. In order to take advantage of them, you'll have to modify a few more lines in `app/config/bootstrap.php`. + +### Graph Generation + +If you'd like to enable graph generation, you will have to do the following: + +1. Within the `$config` array, change `store_statistics` to `true`. If you'd like, you can keep this set as `false`, though you will have to change the 'Store Statistics' option to 'Yes' on the UI if you want the test statistics to be used in graph generation. +2. Run the migration `app/resource/migration/01_CreateSchema.sql` against a MySQL database. + - Note that this will automatically create a database named `vpu` with the tables needed to save your test statistics. +3. Within the `$config` array, change the settings within the `db` array to reflect your database settings. + - Note that if you're using the migration described above, `database` should remain set to `vpu`. + - The `plugin` directive should not be changed. + +### Snapshots + +If you'd like to enable snapshots, you will have to do the following: + +1. Within the `$config` array, change `create_snapshots` to `true`. If you'd like, you can keep this set as `false`, though you will have to change the 'Create Snapshots' option to 'Yes' on the UI if you want the test results to be saved. +2. Within the `$config` array, change `snapshot_directory` to a directory where you would like the snapshots to be saved. + - Note that this directory must have the appropriate permissions in order to allow PHP to write to it. + - Note that the dropdown list on the 'Archives' page will only display the files found within `snapshot_directory`. + +### Error Sandboxing + +If you'd like to enable error sandboxing, you will have to do the following: + +1. Within the `$config` array, change `sandbox_errors` to `true`. If you'd like, you can keep this set as `false`, though you will have to change the 'Sandbox Errors' option to 'Yes' on the UI if you want the errors encountered during the test run to be sandboxed. +2. Within the `$config` array, change `error_reporting` to reflect which errors you'd like to have sandboxed. See PHP's manual entry on [error_reporting](http://php.net/manual/en/function.error-reporting.php) for more information. + +### Ignore Hidden Folders + +By default, the file selector does not display hidden folders (i.e., folders with a '.' prefix). If you'd like to display hidden folders, you will have to do the following: + +1. Within the `$config` array, change `ignore_hidden_folders` to `false`. + +### PHPUnit XML Configuration File + +If you'd like to use a [PHPUnit XML configuration file](http://www.phpunit.de/manual/current/en/appendixes.configuration.html) to define which tests to run, you will have to do the following: + +1. Within the `$config` array, change `xml_configuration_files` to reflect the location(s) where the configuration file(s) can be found. +2. Modify your PHPUnit XML configuration file(s) to include this block: + +```xml + + + + +``` + +### Bootstraps + +If you'd like to load any bootstraps, you will have to do the following: + +1. Within the `$config` array, list the paths to each of the bootstraps within the `bootstraps` array. + +## Keyboard Shortcuts + +### Home Page + +`t - Run Tests` + +## Running VPU at the Command Line + +VPU can be run at the command line, making it possible to automate the generation of test results via cron. + +### Usage + +```bash +# from the project root +bin/vpu --xml_configuration_file app/config/phpunit.xml --snapshot_directory app/history -e -s +``` + +### Options + +-f, --xml_configuration_file: The path to the [XML configuration file](#xml-configuration). Required. Please be sure that the [configuration file](#xml-configuration) contains the required JSON listener. + +-d, --snapshot_directory: The path where the [snapshot](#snapshots) should be stored. Optional. Defaults to the value of `snapshot_directory` within the `$config` array of `app/config/bootstrap`. + +-e, --sandbox_errors: Whether or not to [sandbox](#sandboxing) PHP errors. Optional. Defaults to the value of `sandbox_errors` within the `$config` array of `app/config/bootstrap`. + +-s, --store_statistics: Whether or not to store the statistics in a database. Optional. Defaults to the value of `store_statistics` within the `$config` array of `app/config/bootstrap`. Make sure that the [database](#graph-generation) is configured correctly. + +## Version Information + +Current stable release is v2.2, last updated on May 11, 2013. + +## Credits + +Special thanks to Matt Mueller (http://mattmueller.me/blog/), who came up with the initial concept, wrote the original code (https://github.com/MatthewMueller/PHPUnit-Test-Report), and was kind enough to share it. + +Thanks to Mike Zhou, Hang Dao, Thomas Ingham, and Fredrik Wollsén for their suggestions! diff --git a/vpu/app/config/Routes.php b/vpu/app/config/Routes.php new file mode 100644 index 0000000..5bb457c --- /dev/null +++ b/vpu/app/config/Routes.php @@ -0,0 +1,45 @@ +call('index', $request); + }), + + array('get', '/archives', function($request) { + $controller = new \app\controller\Archives(); + return $controller->call('index', $request); + }), + + array(array('get', 'post'), '/graphs', function($request) { + $controller = new \app\controller\Graph(); + return $controller->call('index', $request); + }), + + array('get', '/file-list', function($request) { + $controller = new \app\controller\FileList(); + return $controller->call('index', $request); + }), + + array('get', '/help', function($request) { + $controller = new \app\controller\Home(); + return $controller->call('help', $request); + }), + + // 404 + array('get', '*', function($request) { + return array( + 'status' => 404, + 'body' => '

Not Found

' + ); + }) + ); + } +} + +?> diff --git a/vpu/app/config/bootstrap.php b/vpu/app/config/bootstrap.php new file mode 100644 index 0000000..590740b --- /dev/null +++ b/vpu/app/config/bootstrap.php @@ -0,0 +1,100 @@ + 'C:\xampp\php', + + // The directories where the tests reside + 'test_directories' => array( + "C:/xampp/htdocs/app/tests" + ), + + + /* Optional */ + + // Whether or not to store the statistics in a database + // (these statistics will be used to generate graphs) + 'store_statistics' => false, + + // The database configuration + 'db' => array( + // MySQL is currently the only database supported + // (do not change this) + 'plugin' => '\app\lib\PDO_MySQL', + + 'database' => 'vpu', + 'host' => 'localhost', + 'port' => '3306', + 'username' => 'root', + 'password' => 'admin' + ), + + // Whether or not to create snapshots of the test results + 'create_snapshots' => false, + + // The directory where the test results will be stored + 'snapshot_directory' => "{$root}/app/history/", + + // Whether or not to sandbox PHP errors + 'sandbox_errors' => false, + + // Which errors to sandbox + // + // (note that E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, + // E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT cannot + // be sandboxed) + // + // see the following for more information: + // http://us3.php.net/manual/en/errorfunc.constants.php + // http://us3.php.net/manual/en/function.error-reporting.php + // http://us3.php.net/set_error_handler + 'error_reporting' => E_ALL | E_STRICT, + + // Whether or not to ignore hidden folders + // (i.e., folders with a '.' prefix) + 'ignore_hidden_folders' => true, + + // The PHPUnit XML configuration files to use + // (leave empty to disable) + // + // In order for VPU to function correctly, the configuration files must + // contain a JSON listener (see the README for more information) + //'xml_configuration_files' => array(), + 'xml_configuration_files' => array( + "C:/xampp/htdocs/phpunit.xml" + ), + + // Paths to any necessary bootstraps + 'bootstraps' => array( + // '/path/to/bootstrap.php' + ) +); + +set_include_path( + get_include_path() + . PATH_SEPARATOR . $root + . PATH_SEPARATOR . $config['pear_path'] +); + +require_once 'PHPUnit/Autoload.php'; +require_once 'PHPUnit/Util/Log/JSON.php'; + +spl_autoload_register(function($class) use ($root) { + $class = str_replace('\\', '/', $class); + $file = "{$root}/{$class}.php"; + if ( file_exists($file) ) { + require $file; + } +}); + +foreach ( $config['bootstraps'] as $bootstrap ) { + require $bootstrap; +} + +\app\lib\Library::store($config); + +?> diff --git a/vpu/app/config/phpunit.xml b/vpu/app/config/phpunit.xml new file mode 100644 index 0000000..629b44a --- /dev/null +++ b/vpu/app/config/phpunit.xml @@ -0,0 +1,16 @@ + + + + + + + /srv/http/vpu/app/test + + + + + + + + + diff --git a/vpu/app/controller/Archives.php b/vpu/app/controller/Archives.php new file mode 100644 index 0000000..47e15ad --- /dev/null +++ b/vpu/app/controller/Archives.php @@ -0,0 +1,40 @@ +is('ajax') ) { + $snapshots = array(); + $handler = @opendir($snapshot_directory); + if ( !$handler ) { + return compact('snapshots'); + } + while ( ($file = readdir($handler)) !== false ) { + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if ( strpos($file, '.') === 0 || $ext != 'html' ) { + continue; + } + $snapshots[] = $file; + } + closedir($handler); + rsort($snapshots); + + return compact('snapshots'); + } + + if ( !isset($request->query['snapshot']) ) { + return ''; + } + + $file = realpath($snapshot_directory) + . "/{$request->query['snapshot']}"; + return ( file_exists($file) ) ? file_get_contents($file) : ''; + } + +} + +?> diff --git a/vpu/app/controller/FileList.php b/vpu/app/controller/FileList.php new file mode 100644 index 0000000..bc442d1 --- /dev/null +++ b/vpu/app/controller/FileList.php @@ -0,0 +1,71 @@ +is('ajax') ) { + return $this->redirect('/'); + } + + $dir = realpath(urldecode($request->query['dir'])); + if ( !$dir ) { + return array(); + } + + $test_directories = \app\lib\Library::retrieve('test_directories'); + $valid_dir = false; + foreach ( $test_directories as $test_directory ) { + if ( strpos($dir, realpath($test_directory)) === 0 ) { + $valid_dir = true; + break; + } + } + + if ( !$valid_dir ) { + return array(); + } + + $dir .= '/'; + $files = scandir($dir); + // Don't return anything if 'files' are '.' or '..' + if ( count($files) < 3 ) { + return array(); + } + + $ignore_hidden = \app\lib\Library::retrieve('ignore_hidden_folders'); + + $final_dirs = array(); + $final_files = array(); + foreach ( $files as $file ) { + $is_hidden = ( strpos($file, '.') === 0 ); + if ( + $file != '.' && $file != '..' + && (!$is_hidden || !$ignore_hidden) + ) { + $path = $dir . $file; + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + if ( is_dir($path) ) { + $final_dirs[] = array( + 'type' => 'directory', + 'name' => $file, + 'path' => $path + ); + } elseif ( is_file($path) && $ext == 'php' ) { + $final_files[] = array( + 'type' => 'file', + 'name' => $file, + 'path' => $path + ); + } + } + } + + return array_merge($final_dirs, $final_files); + } + +} + +?> diff --git a/vpu/app/controller/Graph.php b/vpu/app/controller/Graph.php new file mode 100644 index 0000000..2975a14 --- /dev/null +++ b/vpu/app/controller/Graph.php @@ -0,0 +1,123 @@ +is('get') ) { + return array(); + } + + $table = "{$request->data['graph_type']}Result"; + if ( + !$request->data['start_date'] || !$request->data['end_date'] + || ($table != 'SuiteResult' && $table != 'TestResult') + ) { + return array( + 'type' => $request->data['graph_type'], + 'timeFrame' => $request->data['time_frame'], + 'categories' => array(), + 'failed' => 0, + 'succeeded' => 0, + 'skipped' => 0, + 'incomplete' => 0, + ); + } + + $db_options = \app\lib\Library::retrieve('db'); + $db = new $db_options['plugin'](); + if ( !$db->connect($db_options) ) { + return array( + 'error' => array( + 'title' => 'Error Connecting to Database', + 'message' => implode(' ', $db->get_errors()) + ) + ); + } + + switch ( $request->data['time_frame'] ) { + case 'Monthly': + $interval = 2678400; + $sql_format = 'Y-m-01'; + $output = 'M Y'; + break; + case 'Weekly': + $interval = 604800; + $sql_format = 'Y-m-d'; + $output = 'm/d'; + break; + default: + $interval = 86400; + $sql_format = 'Y-m-d'; + $output = 'm/d'; + break; + } + $current = $start = strtotime($request->data['start_date']); + $end = strtotime($request->data['end_date']) + $interval; + + $categories = array(); + $plot_values = array( + 'failed' => array(), + 'incomplete' => array(), + 'skipped' => array(), + 'succeeded' => array() + ); + while ( $current < $end ) { + $categories[] = date($output, $current); + $next = $current + $interval; + + $data = array( + 'failed' => 0, + 'incomplete' => 0, + 'skipped' => 0, + 'succeeded' => 0 + ); + + $sql = "select failed, incomplete, skipped, succeeded " + . "from {$table} where run_date >= ? and run_date < ?"; + $params = array( + date($sql_format, $current), + date($sql_format, $next) + ); + $db->query($sql, $params); + + $results = $db->fetch_all(); + $num_rows = count($results); + + if ( $num_rows > 0 ) { + foreach ( $results as $result ) { + foreach ( $result as $key => $value ) { + $data[$key] += $value; + } + } + } + + foreach ( $data as $key => $val ) { + if ( $num_rows > 0 ) { + $plot_values[$key][] = round($val / $num_rows, 2); + } else { + $plot_values[$key][] = 0; + } + } + + $current = $next; + } + + $db->close(); + + return array( + 'type' => $request->data['graph_type'], + 'timeFrame' => $request->data['time_frame'], + 'categories' => $categories, + 'failed' => $plot_values['failed'], + 'succeeded' => $plot_values['succeeded'], + 'skipped' => $plot_values['skipped'], + 'incomplete' => $plot_values['incomplete'] + ); + } + +} + +?> diff --git a/vpu/app/controller/Home.php b/vpu/app/controller/Home.php new file mode 100644 index 0000000..cf3be4b --- /dev/null +++ b/vpu/app/controller/Home.php @@ -0,0 +1,159 @@ +render_html('partial/test_results', $view_data); + + $handle = @fopen($filename, 'a'); + if ( !file_exists($directory) || !$handle ) { + return array( + 'type' => 'failed', + 'title' => 'Error Creating Snapshot', + 'message' => 'Please ensure that the ' + . 'snapshot_directory in ' + . 'app/config/bootstrap.php exists and ' + . 'has the proper permissions.' + ); + } + + fwrite($handle, $contents); + fclose($handle); + return array( + 'type' => 'succeeded', + 'title' => 'Snapshot Created', + 'message' => "Snapshot can be found at {$filename}." + ); + } + + // GET + public function help($request) { + return array(); + } + + // GET/POST + public function index($request) { + if ( $request->is('get') ) { + $normalize_path = function($path) { + return str_replace('\\', '/', realpath($path)); + }; + $test_directories = json_encode(array_map( + $normalize_path, \app\lib\Library::retrieve('test_directories') + )); + + $suites = array(); + $stats = array(); + $store_statistics = \app\lib\Library::retrieve('store_statistics'); + $create_snapshots = \app\lib\Library::retrieve('create_snapshots'); + $sandbox_errors = \app\lib\Library::retrieve('sandbox_errors'); + $xml_configuration_files = \app\lib\Library::retrieve( + 'xml_configuration_files' + ); + return compact( + 'create_snapshots', + 'sandbox_errors', + 'stats', + 'store_statistics', + 'suites', + 'test_directories', + 'xml_configuration_files' + ); + } + + $tests = explode('|', $request->data['test_files']); + $vpu = new \app\lib\VPU(); + + if ( $request->data['sandbox_errors'] ) { + error_reporting(\app\lib\Library::retrieve('error_reporting')); + set_error_handler(array($vpu, 'handle_errors')); + } + + $xml_config = false; + + $notifications = array(); + if ( $xml_file_index = $request->data['xml_configuration_file'] ) { + $files = \app\lib\Library::retrieve('xml_configuration_files'); + $xml_config = $files[$xml_file_index - 1]; + if ( !$xml_config || !$xml_config = realpath($xml_config) ) { + $notifications[] = array( + 'type' => 'failed', + 'title' => 'No Valid XML Configuration File Found', + 'message' => 'Please ensure that the ' + . 'xml_configuration_file in ' + . 'app/config/bootstrap.php exists and ' + . 'has the proper permissions.' + ); + } + } + + $results = ( $xml_config ) + ? $vpu->run_with_xml($xml_config) + : $vpu->run_tests($tests); + $results = $vpu->compile_suites($results, 'web'); + + if ( $request->data['sandbox_errors'] ) { + restore_error_handler(); + } + + $suites = $results['suites']; + $stats = $results['stats']; + $errors = $vpu->get_errors(); + $to_view = compact('suites', 'stats', 'errors'); + + if ( $request->data['create_snapshots'] ) { + $notifications[] = $this->_create_snapshot($to_view); + } + if ( $request->data['store_statistics'] ) { + $notifications[] = $this->_store_statistics($stats); + } + + return $to_view + compact('notifications'); + } + + protected function _store_statistics($stats) { + $db_options = \app\lib\Library::retrieve('db'); + $db = new $db_options['plugin'](); + if ( !$db->connect($db_options) ) { + return array( + 'type' => 'failed', + 'title' => 'Error Connecting to Database', + 'message' => implode(' ', $db->get_errors()) + ); + } + + $now = date('Y-m-d H:i:s'); + foreach ( $stats as $key => $stat ) { + $data = array( + 'run_date' => $now, + 'failed' => $stat['failed'], + 'incomplete' => $stat['incomplete'], + 'skipped' => $stat['skipped'], + 'succeeded' => $stat['succeeded'] + ); + $table = ucfirst(rtrim($key, 's')) . 'Result'; + if ( !$db->insert($table, $data) ) { + return array( + 'type' => 'failed', + 'title' => 'Error Inserting Record', + 'message' => implode(' ', $db->get_errors()) + ); + } + } + + return array( + 'type' => 'succeeded', + 'title' => 'Statistics Stored', + 'message' => 'The statistics generated during this test run were ' + . 'successfully stored.' + ); + + } + +} + +?> diff --git a/vpu/app/core/Controller.php b/vpu/app/core/Controller.php new file mode 100644 index 0000000..6701284 --- /dev/null +++ b/vpu/app/core/Controller.php @@ -0,0 +1,140 @@ + array( + 'view' => 'app\core\View' + ) + ); + $this->_config = $config + $defaults; + } + + /** + * Primary entry point for all controller actions. The supplied action is + * called, returning a response which is then filtered based on the request + * source (e.g., xhr, web). + * + * @param string $action The method to be called. + * @param obj $request The request object. + * @access public + * @return array + */ + public function call($action, $request) { + $results = $this->$action($request); + + if ( is_null($results) || $results === false ) { + return false; + } + + if ( !is_array($results) ) { + $this->_response['body'] = $results; + return $this->_response; + } + + if ( $request->is('ajax') ) { + $this->_response['body'] = $this->render_json($results); + } else { + $class = explode('\\', get_called_class()); + $classname = end($class); + $file = lcfirst($classname) . "/{$action}"; + $this->_response['body'] = $this->render_html($file, $results); + } + + return $this->_response; + } + + /** + * Redirects the page. + * + * @param string $page The page to be redirected to. + * @access public + * @return bool + */ + public function redirect($page) { + $this->set_response_status(303); + $this->set_response_headers(array('Location: ' . $page)); + return ''; + } + + /** + * Converts the supplied value to JSON. + * + * @param mixed $value The value to encode. + * @access public + * @return string + */ + public function render_json($value) { + $options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP; + return json_encode($value, $options); + } + + /** + * Renders a view. + * + * @param string $action The file to be rendered. + * @param array $vars The variables to be substituted in the view. + * @access public + * @return string + */ + public function render_html($file, $vars = array()) { + $view = $this->_config['dependencies']['view']; + $view = new $view(); + return $view->render($file, $vars); + } + + /** + * Sets the response headers. Note that the supplied headers must be + * well-formed HTTP headers. Example: + * + * $headers = array('Content-Type: text/html; charset=utf-8'); + * + * @param array $headers The response headers. + * @access public + * @return void + */ + public function set_response_headers($headers) { + $this->_response['headers'] = $headers; + } + + /** + * Sets the response status. Note that the supplied status must be the + * integer associated with the HTTP status code (e.g., 404 for Not Found). + * + * @param int $status The response status. + * @access public + * @return void + */ + public function set_response_status($status) { + $this->_response['status'] = $status; + } + +} + +?> diff --git a/vpu/app/core/View.php b/vpu/app/core/View.php new file mode 100644 index 0000000..6790ae0 --- /dev/null +++ b/vpu/app/core/View.php @@ -0,0 +1,70 @@ + array( + 'compiler' => 'app\lib\Compiler', + ) + ); + $this->_config = $config + $defaults; + } + + /** + * Escapes a value for output in an HTML context. + * + * @param mixed $value + * @access public + * @return mixed + */ + public function escape($value) { + return nl2br(htmlspecialchars($value, ENT_QUOTES, 'UTF-8')); + } + + /** + * Renders a given file with the supplied variables. + * + * @param string $file The file to be rendered. + * @param mixed $vars The variables to be substituted in the view. + * @access public + * @return string + */ + public function render($file, $vars = null) { + $path = dirname(__DIR__) . '/resource/cache/'; + $file = dirname(__DIR__) . "/view/{$file}.html"; + + $compiler = $this->_config['dependencies']['compiler']; + $options = compact('path'); + $__template__ = $compiler::compile($file, $options); + + if ( !$__template__ ) { + throw new \RuntimeException( + 'Could not write cache file. Please ensure that the ' + . "permissions of {$path} are correct." + ); + } + + if ( is_array($vars) ) { + extract($vars); + } + + ob_start(); + require $__template__; + return ob_get_clean(); + } + +} + +?> diff --git a/vpu/app/lib/Compiler.php b/vpu/app/lib/Compiler.php new file mode 100644 index 0000000..0dcda25 --- /dev/null +++ b/vpu/app/lib/Compiler.php @@ -0,0 +1,82 @@ + 'compiled/' + ); + + $stats = stat($file); + $dir = dirname($file); + $location = basename(dirname($dir)) . '_' . basename($dir) + . '_' . basename($file, '.html'); + $template = 'template_' . $location . '_' . $stats['mtime'] + . '_' . $stats['ino'] . '_' . $stats['size'] . '.html'; + $template = $options['path'] . $template; + + if ( file_exists($template) ) { + return $template; + } + + $compiled = self::_replace(file_get_contents($file)); + $template_dir = dirname($template); + if ( !is_dir($template_dir) && !mkdir($template_dir, 0755, true) ) { + return false; + } + + if ( + !is_writable($template_dir) + || file_put_contents($template, $compiled) === false + ) { + return false; + } + + $pattern = $template_dir . '/template_' . $location . '_*.html'; + foreach ( glob($pattern) as $old ) { + if ( $old !== $template ) { + unlink($old); + } + } + return $template; + } + + /** + * Replaces a template with custom syntax. + * + * @param string $template The template. + * @access public + * @return string + */ + protected static function _replace($template) { + $replace = array( + '/\<\?=\s*\$this->(.+?)\s*;?\s*\?>/msx' => + '$1; ?>', + + '/\$e\((.+?)\)\s*;/msx' => + 'echo $this->escape($1);', + + '/\<\?=\s*(.+?)\s*;?\s*\?>/msx' => + 'escape($1); ?>' + ); + + return preg_replace( + array_keys($replace), array_values($replace), $template + ); + } + +} diff --git a/vpu/app/lib/Library.php b/vpu/app/lib/Library.php new file mode 100644 index 0000000..5b7b4fa --- /dev/null +++ b/vpu/app/lib/Library.php @@ -0,0 +1,44 @@ + diff --git a/vpu/app/lib/PDO_MySQL.php b/vpu/app/lib/PDO_MySQL.php new file mode 100644 index 0000000..692d807 --- /dev/null +++ b/vpu/app/lib/PDO_MySQL.php @@ -0,0 +1,319 @@ +_affected_rows; + } + + /** + * Closes the connection. + * + * @access public + * @return bool + */ + public function close() { + $this->_dbh = null; + return true; + } + + /** + * Connects and selects database. + * + * @param array $options Contains the connection information. Takes the + * following options: + * 'database' - The name of the database. + * 'host' - The database host. + * 'port' - The database port. + * 'username' - The database username. + * 'password' - The database password. + * @access public + * @return bool + */ + public function connect($options = array()) { + $dsn = "mysql:host={$options['host']};port={$options['port']}" + . ";dbname={$options['database']}"; + try { + $this->_dbh = new PDO($dsn, $options['username'], $options['password']); + $this->_dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + return true; + } catch ( PDOException $e ) { + $this->_errors[] = $e->getMessage(); + return false; + } + } + + /** + * Fetches the next row from the result set in memory (i.e., the one + * that was created after running query()). + * + * @param string $fetch_style Controls how the rows will be returned. + * @param obj $obj The object to be fetched into if + * $fetch_style is set to 'into'. + * @access public + * @return mixed + */ + public function fetch($fetch_style = null, $obj = null) { + $this->_set_fetch_mode($fetch_style, $obj); + $row = $this->_statement->fetch(); + $this->_statement->closeCursor(); + return $row; + } + + /** + * Returns an array containing all of the result set rows. + * + * @param string $fetch_style Controls how the rows will be returned. + * @access public + * @return mixed + */ + public function fetch_all($fetch_style = null) { + $this->_set_fetch_mode($fetch_style); + $rows = $this->_statement->fetchAll(); + $this->_statement->closeCursor(); + return $rows; + } + + /** + * Returns a single column from the next row of a result set or false + * if there are no more rows. + * + * @param int $column_number Zero-index number of the column to + * retrieve from the row. + * @access public + * @return mixed + */ + public function fetch_column($column_number = 0) { + $column = $this->_statement->fetchColumn($column_number); + $this->_statement->closeCursor(); + return $column; + } + + /** + * Returns the errors generated by PDOExceptions. + * + * @access public + * @return array + */ + public function get_errors() { + return $this->_errors; + } + + /** + * Inserts a record into the database. + * + * @param string $table The table containing the record to be inserted. + * @param array $data An array containing the data to be inserted. + * Format should be as follows: + * array('column_name' => 'column_value'); + * @access public + * @return bool + */ + public function insert($table, $data) { + $sql = "INSERT INTO {$table} "; + + $key_names = array_keys($data); + $fields = implode(', ', $key_names); + $values = ':' . implode(', :', $key_names); + + $sql .= "({$fields}) VALUES ({$values})"; + + $statement = $this->_dbh->prepare($sql); + + try { + $statement->execute($data); + } catch ( PDOException $e ) { + $this->_errors[] = $e->getMessage(); + return false; + } + + $this->_affected_rows = $statement->rowCount(); + return true; + } + + /** + * Returns the ID of the last inserted row or sequence value. + * + * @access public + * @return int + */ + public function insert_id() { + return $this->_dbh->lastInsertId(); + } + + /** + * Executes SQL query. + * + * @param string $sql The SQL query to be executed. + * @param array $parameters An array containing the parameters to be + * bound. + * @access public + * @return bool + */ + public function query($sql, $parameters = array()) { + $statement = $this->_dbh->prepare($sql); + + foreach ( $parameters as $index => $parameter ) { + $statement->bindValue($index + 1, $parameter); + } + + try { + $statement->execute(); + } catch ( PDOException $e ) { + $this->_errors[] = $e->getMessage(); + return false; + } + + $this->_affected_rows = $statement->rowCount(); + $this->_statement = $statement; + return true; + } + + /** + * Sets the fetch mode. + * + * @param string $fetch_style Controls how the rows will be returned. + * @param obj $obj The object to be fetched into for use with + * FETCH_INTO. + * @access protected + * @return int + */ + protected function _set_fetch_mode($fetch_style, $obj = null) { + switch ( $fetch_style ) { + case 'assoc': + $this->_statement->setFetchMode(PDO::FETCH_ASSOC); + break; + case 'into': + $this->_statement->setFetchMode(PDO::FETCH_INTO, $obj); + break; + default: + $this->_statement->setFetchMode(PDO::FETCH_ASSOC); + break; + } + } + + /** + * Updates a record in the database. + * + * @param string $table The table containing the record to be inserted. + * @param array $data An array containing the data to be inserted. + * Format should be as follows: + * array('column_name' => 'column_value'); + * @param array $where The WHERE clause of the SQL query. + * @access public + * @return bool + */ + public function update($table, $data, $where = null) { + $sql = "UPDATE {$table} SET "; + + $key_names = array_keys($data); + foreach ( $key_names as $name ) { + $sql .= "{$name}=:{$name}, "; + } + + $sql = rtrim($sql, ', '); + + if ( !is_null($where) ) { + $sql .= ' WHERE '; + foreach ( $where as $name => $val ) { + $sql .= "{$name}=:{$name}_where, "; + $data["{$name}_where"] = $val; + } + } + $statement = $this->_dbh->prepare($sql); + + try { + $statement->execute($data); + } catch ( PDOException $e ) { + $this->_errors[] = $e->getMessage(); + return false; + } + + $this->_affected_rows = $statement->rowCount(); + return true; + } + + /** + * Inserts or updates (if exists) a record in the database. + * + * @param string $table The table containing the record to be inserted. + * @param array $data An array containing the data to be inserted. + * Format should be as follows: + * array('column_name' => 'column_value'); + * @access public + * @return bool + */ + public function upsert($table, $data) { + $sql = "INSERT INTO {$table}"; + + $key_names = array_keys($data); + $fields = implode(', ', $key_names); + $values = ':' . implode(', :', $key_names); + + + $sql .= "({$fields}) VALUES ({$values}) ON DUPLICATE KEY UPDATE "; + + foreach ( $key_names as $name ) { + $sql .= "{$name}=:{$name}, "; + } + + $sql = rtrim($sql, ', '); + $statement = $this->_dbh->prepare($sql); + + try { + $statement->execute($data); + } catch ( PDOException $e ) { + $this->_errors[] = $e->getMessage(); + return false; + } + + $this->_affected_rows = $statement->rowCount(); + return true; + } +} + +?> diff --git a/vpu/app/lib/VPU.php b/vpu/app/lib/VPU.php new file mode 100644 index 0000000..8bddb19 --- /dev/null +++ b/vpu/app/lib/VPU.php @@ -0,0 +1,507 @@ + $stats ) { + $results[$name] = $stats; + foreach ( $stats as $key => $value ) { + if ( $key == 'total' ) { + continue; + } + // Avoid divide by zero error + if ( $stats['total'] ) { + $results[$name]['percent' . ucfirst($key)] = + round($stats[$key] / $stats['total'] * 100, 1); + } else { + $results[$name]['percent' . ucfirst($key)] = 0; + } + } + } + + return $results; + } + + /** + * Returns the class name without the namespace. + * + * @param string $class The class name. + * @access protected + * @return string + */ + protected function _classname_only($class) { + $name = explode('\\', $class); + return end($name); + } + + /** + * Organizes the output from PHPUnit into a more manageable array + * of suites and statistics. + * + * @param string $pu_output The JSON output from PHPUnit. + * @param string $source The executing source (web or cli). + * @access public + * @return array + */ + public function compile_suites($pu_output, $source) { + $results = $this->_parse_output($pu_output); + + $collection = array(); + $statistics = array( + 'suites' => array( + 'succeeded' => 0, + 'skipped' => 0, + 'incomplete' => 0, + 'failed' => 0, + 'total' => 0 + ) + ); + $statistics['tests'] = $statistics['suites']; + foreach ( $results as $result ) { + if ( !isset($result['event']) || $result['event'] != 'test' ) { + continue; + } + + $suite_name = $this->_classname_only($result['suite']); + + if ( !isset($collection[$suite_name]) ) { + $collection[$suite_name] = array( + 'tests' => array(), + 'name' => $suite_name, + 'status' => 'succeeded', + 'time' => 0 + ); + } + $result = $this->_format_test_results($result, $source); + $collection[$suite_name]['tests'][] = $result; + $collection[$suite_name]['status'] = $this->_get_suite_status( + $result['status'], $collection[$suite_name]['status'] + ); + $collection[$suite_name]['time'] += $result['time']; + $statistics['tests'][$result['status']] += 1; + $statistics['tests']['total'] += 1; + } + + foreach ( $collection as $suite ) { + $statistics['suites'][$suite['status']] += 1; + $statistics['suites']['total'] += 1; + } + + $final = array( + 'suites' => $collection, + 'stats' => $this->_add_percentages($statistics) + ); + + return $final; + } + + /** + * Converts the first nested layer of PHPUnit-generated JSON to an + * associative array. + * + * @param string $str The JSON output from PHPUnit. + * @access protected + * @return array + */ + protected function _convert_json($str) { + $str = str_replace('"', '"', $str); + + $tags = array(); + $nest = 0; + $start_mark = 0; + $in_quotes = false; + + $length = strlen($str); + for ( $i = 0; $i < $length; $i++ ) { + $char = $str{$i}; + + if ( $char == '"' ) { + // Escaped quote in debug output + if ( !$in_quotes || $str{$i - 1} == "\\" ) { + $i = strpos($str, '"', $i + 1) - 1; + $in_quotes = true; + } else { + $in_quotes = false; + } + continue; + } + + if ( $char == '{' ) { + $nest++; + if ( $nest == 1 ) { + $start_mark = $i; + } + } elseif ( $char == '}' && $nest > 0 ) { + if ( $nest == 1 ) { + $tags[] = substr( + $str, $start_mark + 1, $i - $start_mark - 1 + ); + $start_mark = $i; + } + $nest--; + } + } + + return $tags; + } + + /** + * Normalizes the test results. + * + * @param array $test_results The parsed test results. + * @param string $source The executing source (web or cli). + * @access protected + * @return string + */ + protected function _format_test_results($test_results, $source) { + $status = $this->_get_test_status( + $test_results['status'], $test_results['message'] + ); + $name = substr( + $test_results['test'], strpos($test_results['test'], '::') + 2 + ); + $time = $test_results['time']; + $message = $test_results['message']; + $output = ( isset($test_results['output']) ) + ? trim($test_results['output']) + : ''; + $trace = $this->_get_trace($test_results['trace'], $source); + + return compact( + 'status', + 'name', + 'time', + 'message', + 'output', + 'trace' + ); + } + + /** + * Returns the errors collected by the custom error handler. + * + * @access public + * @return array + */ + public function get_errors() { + return $this->_errors; + } + + /** + * Determines the overall suite status based on the current status + * of the suite and the status of a single test. + * + * @param string $test_status The status of the test. + * @param string $suite_status The current status of the suite. + * @access protected + * @return string + */ + protected function _get_suite_status($test_status, $suite_status) { + if ( + $test_status === 'incomplete' && $suite_status !== 'failed' + && $suite_status !== 'skipped' + ) { + return 'incomplete'; + } + if ( $test_status === 'skipped' && $suite_status !== 'failed' ) { + return 'skipped'; + } + if ( $test_status === 'failed' ) { + return 'failed'; + } + return $suite_status; + } + + /** + * Retrieves the status from a PHPUnit test result. + * + * @param string $status The status supplied by VPU's transformed JSON. + * @param string $message The message supplied by VPU's transformed JSON. + * @access protected + * @return string + */ + protected function _get_test_status($status, $message) { + switch ( $status ) { + case 'pass': + return 'succeeded'; + case 'error': + if ( stripos($message, 'skipped') !== false ) { + return 'skipped'; + } + if ( stripos($message, 'incomplete') !== false ) { + return 'incomplete'; + } + return 'failed'; + case 'fail': + return 'failed'; + default: + return ''; + } + } + + /** + * Filters the stack trace from a PHPUnit test result to exclude VPU's + * trace. + * + * @param string $stack The stack trace. + * @param string $source The executing source (web or cli). + * @access protected + * @return string + */ + protected function _get_trace($stack, $source) { + if ( !$stack ) { + return ''; + } + + ob_start(); + if ( $source == 'web' ) { + print_r(array_slice($stack, 0, -6)); + } else { + print_r(array_slice($stack, 0, -2)); + } + $trace = trim(ob_get_contents()); + ob_end_clean(); + + return $trace; + } + + /** + * Serves as the error handler. + * + * @param int $number The level of the error raised. + * @param string $message The error message. + * @param string $file The file in which the error was raised. + * @param int $line The line number at which the error was raised. + * @access public + * @return bool + */ + public function handle_errors($number, $message, $file, $line) { + if ( $number > error_reporting() ) { + return true; + } + + switch ( $number ) { + case E_WARNING: + $type = 'E_WARNING'; + break; + case E_NOTICE: + $type = 'E_NOTICE'; + break; + case E_USER_ERROR: + $type = 'E_USER_ERROR'; + break; + case E_USER_WARNING: + $type = 'E_USER_WARNING'; + break; + case E_USER_NOTICE: + $type = 'E_USER_NOTICE'; + break; + case E_STRICT: + $type = 'E_STRICT'; + break; + case E_RECOVERABLE_ERROR: + $type = 'E_RECOVERABLE_ERROR'; + break; + case E_DEPRECATED: + $type = 'E_DEPRECATED'; + break; + case E_USER_DEPRECATED: + $type = 'E_USER_DEPRECATED'; + break; + default: + $type = 'Unknown'; + break; + } + $this->_errors[] = compact('type', 'message', 'file', 'line'); + return true; + } + + /** + * Parses and formats the JSON output from PHPUnit into an associative array. + * + * @param string $pu_output The JSON output from PHPUnit. + * @access protected + * @return array + */ + protected function _parse_output($pu_output) { + $results = ''; + foreach ( $this->_convert_json($pu_output) as $elem ) { + $elem = '{' . $elem . '}'; + $pos = strpos($pu_output, $elem); + $pu_output = substr_replace($pu_output, '|||', $pos, strlen($elem)); + $results .= $elem . ','; + } + + $results = '[' . rtrim($results, ',') . ']'; + + $results = json_decode($results, true); + + // For PHPUnit 3.5.x, which doesn't include test output in the JSON + $pu_output = explode('|||', $pu_output); + foreach ( $pu_output as $key => $data ) { + if ( $data ) { + $results[$key]['output'] = $data; + } + } + + return $results; + } + + /** + * Retrieves the files from any supplied directories, and filters + * the list of tests by ensuring that the files exist and are PHP files. + * + * @param array $tests The directories/filenames containing the tests to + * be run through PHPUnit. + * @access protected + * @return array + */ + protected function _parse_tests($tests) { + $collection = array(); + + foreach ( $tests as $test ) { + if ( is_dir($test) ) { + $it = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator(realpath($test)), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + while ( $it->valid() ) { + $ext = strtolower(pathinfo($it->key(), PATHINFO_EXTENSION)); + if ( !$it->isDot() && $ext == 'php' ) { + $collection[] = $it->key(); + } + + $it->next(); + } + continue; + } + + $ext = strtolower(pathinfo($test, PATHINFO_EXTENSION)); + if ( file_exists($test) && $ext == 'php' ) { + $collection[] = realpath($test); + } + } + // Avoid returning duplicates + return array_keys(array_flip($collection)); + } + + /** + * Runs supplied tests through PHPUnit. + * + * @param array $tests The directories/filenames containing the tests + * to be run through PHPUnit. + * @access public + * @return string + */ + public function run_tests($tests) { + $suite = new \PHPUnit_Framework_TestSuite(); + + $tests = $this->_parse_tests($tests); + $original_classes = get_declared_classes(); + foreach ( $tests as $test ) { + require $test; + } + $new_classes = get_declared_classes(); + $tests = array_diff($new_classes, $original_classes); + foreach ( $tests as $test ) { + if ( is_subclass_of($test, 'PHPUnit_Framework_TestCase') ) { + $suite->addTestSuite($test); + } + } + + $result = new \PHPUnit_Framework_TestResult(); + $result->addListener(new \PHPUnit_Util_Log_JSON()); + + // We need to temporarily turn off html_errors to ensure correct + // parsing of test debug output + $html_errors = ini_get('html_errors'); + ini_set('html_errors', 0); + + ob_start(); + $suite->run($result); + $results = ob_get_contents(); + ob_end_clean(); + + ini_set('html_errors', $html_errors); + return $results; + } + + /** + * Checks that the provided XML configuration file contains the necessary + * JSON listener. + * + * @param string $xml_config The path to the PHPUnit XML configuration + * file. + * @access protected + * @return void + */ + protected function _check_xml_configuration($xml_config) { + $configuration = \PHPUnit_Util_Configuration::getInstance($xml_config); + $listeners = $configuration->getListenerConfiguration(); + + $required_listener = 'PHPUnit_Util_Log_JSON'; + $found = false; + foreach ( $listeners as $listener ) { + if ( $listener['class'] === $required_listener ) { + $found = true; + break; + } + } + if ( !$found ) { + throw new \DomainException( + "XML Configuration file doesn't contain the required " . + "{$required_listener} listener." + ); + } + } + + /** + * Runs PHPUnit with the supplied XML configuration file. + * + * @param string $xml_config The path to the PHPUnit XML configuration + * file. + * @access public + * @return string + */ + public function run_with_xml($xml_config) { + $this->_check_xml_configuration($xml_config); + $command = new \PHPUnit_TextUI_Command(); + + // We need to temporarily turn off html_errors to ensure correct + // parsing of test debug output + $html_errors = ini_get('html_errors'); + ini_set('html_errors', 0); + + ob_start(); + $command->run(array('--configuration', $xml_config, '--stderr'), false); + $results = ob_get_contents(); + ob_end_clean(); + + ini_set('html_errors', $html_errors); + + $start = strpos($results, '{'); + $end = strrpos($results, '}'); + return substr($results, $start, $end - $start + 1); + } + +} + +?> diff --git a/vpu/app/public/.htaccess b/vpu/app/public/.htaccess new file mode 100644 index 0000000..c68f87a --- /dev/null +++ b/vpu/app/public/.htaccess @@ -0,0 +1,8 @@ + + RewriteEngine On + RewriteBase /vpu/app/public + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !favicon.ico$ + RewriteRule ^(.*)$ index.php [QSA,L] + diff --git a/vpu/app/public/css/bootstrap-responsive.css b/vpu/app/public/css/bootstrap-responsive.css new file mode 100644 index 0000000..7f669d5 --- /dev/null +++ b/vpu/app/public/css/bootstrap-responsive.css @@ -0,0 +1,808 @@ +/*! + * Bootstrap Responsive v2.0.3 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 28px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +@media (max-width: 767px) { + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 18px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-group > label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .modal { + position: absolute; + top: 10px; + right: 10px; + left: 10px; + width: auto; + margin: 0; + } + .modal.fade.in { + top: auto; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + [class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: auto; + margin-left: 0; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 28px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 20px; + } + .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 28px; + margin-left: 2.762430939%; + *margin-left: 2.709239449638298%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span12 { + width: 99.999999993%; + *width: 99.9468085036383%; + } + .row-fluid .span11 { + width: 91.436464082%; + *width: 91.38327259263829%; + } + .row-fluid .span10 { + width: 82.87292817100001%; + *width: 82.8197366816383%; + } + .row-fluid .span9 { + width: 74.30939226%; + *width: 74.25620077063829%; + } + .row-fluid .span8 { + width: 65.74585634900001%; + *width: 65.6926648596383%; + } + .row-fluid .span7 { + width: 57.182320438000005%; + *width: 57.129128948638304%; + } + .row-fluid .span6 { + width: 48.618784527%; + *width: 48.5655930376383%; + } + .row-fluid .span5 { + width: 40.055248616%; + *width: 40.0020571266383%; + } + .row-fluid .span4 { + width: 31.491712705%; + *width: 31.4385212156383%; + } + .row-fluid .span3 { + width: 22.928176794%; + *width: 22.874985304638297%; + } + .row-fluid .span2 { + width: 14.364640883%; + *width: 14.311449393638298%; + } + .row-fluid .span1 { + width: 5.801104972%; + *width: 5.747913482638298%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 714px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 652px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 590px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 528px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 466px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 404px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 342px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 280px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 218px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 156px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 94px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 32px; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 30px; + } + .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 28px; + margin-left: 2.564102564%; + *margin-left: 2.510911074638298%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145300001%; + *width: 91.3997999636383%; + } + .row-fluid .span10 { + width: 82.905982906%; + *width: 82.8527914166383%; + } + .row-fluid .span9 { + width: 74.358974359%; + *width: 74.30578286963829%; + } + .row-fluid .span8 { + width: 65.81196581200001%; + *width: 65.7587743226383%; + } + .row-fluid .span7 { + width: 57.264957265%; + *width: 57.2117657756383%; + } + .row-fluid .span6 { + width: 48.717948718%; + *width: 48.6647572286383%; + } + .row-fluid .span5 { + width: 40.170940171000005%; + *width: 40.117748681638304%; + } + .row-fluid .span4 { + width: 31.623931624%; + *width: 31.5707401346383%; + } + .row-fluid .span3 { + width: 23.076923077%; + *width: 23.0237315876383%; + } + .row-fluid .span2 { + width: 14.529914530000001%; + *width: 14.4767230406383%; + } + .row-fluid .span1 { + width: 5.982905983%; + *width: 5.929714493638298%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1160px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1060px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 960px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 860px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 760px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 660px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 560px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 460px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 360px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 260px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 160px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 60px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top { + position: static; + margin-bottom: 18px; + } + .navbar-fixed-top .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 9px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #999999; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 6px 15px; + font-weight: bold; + color: #999999; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .dropdown-menu a:hover { + background-color: #222222; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: block; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 9px 15px; + margin: 9px 0; + border-top: 1px solid #222222; + border-bottom: 1px solid #222222; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/vpu/app/public/css/bootstrap.css b/vpu/app/public/css/bootstrap.css new file mode 100644 index 0000000..8de4b52 --- /dev/null +++ b/vpu/app/public/css/bootstrap.css @@ -0,0 +1,5038 @@ +/*! + * Bootstrap v2.0.3 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 28px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover { + color: #005580; + text-decoration: underline; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + margin-left: 20px; +} + +.container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 28px; + margin-left: 2.127659574%; + *margin-left: 2.0744680846382977%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .span12 { + width: 99.99999998999999%; + *width: 99.94680850063828%; +} + +.row-fluid .span11 { + width: 91.489361693%; + *width: 91.4361702036383%; +} + +.row-fluid .span10 { + width: 82.97872339599999%; + *width: 82.92553190663828%; +} + +.row-fluid .span9 { + width: 74.468085099%; + *width: 74.4148936096383%; +} + +.row-fluid .span8 { + width: 65.95744680199999%; + *width: 65.90425531263828%; +} + +.row-fluid .span7 { + width: 57.446808505%; + *width: 57.3936170156383%; +} + +.row-fluid .span6 { + width: 48.93617020799999%; + *width: 48.88297871863829%; +} + +.row-fluid .span5 { + width: 40.425531911%; + *width: 40.3723404216383%; +} + +.row-fluid .span4 { + width: 31.914893614%; + *width: 31.8617021246383%; +} + +.row-fluid .span3 { + width: 23.404255317%; + *width: 23.3510638276383%; +} + +.row-fluid .span2 { + width: 14.89361702%; + *width: 14.8404255306383%; +} + +.row-fluid .span1 { + width: 6.382978723%; + *width: 6.329787233638298%; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; +} + +p small { + font-size: 11px; + color: #999999; +} + +.lead { + margin-bottom: 18px; + font-size: 20px; + font-weight: 200; + line-height: 27px; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-family: inherit; + font-weight: bold; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + color: #999999; +} + +h1 { + font-size: 30px; + line-height: 36px; +} + +h1 small { + font-size: 18px; +} + +h2 { + font-size: 24px; + line-height: 36px; +} + +h2 small { + font-size: 18px; +} + +h3 { + font-size: 18px; + line-height: 27px; +} + +h3 small { + font-size: 14px; +} + +h4, +h5, +h6 { + line-height: 18px; +} + +h4 { + font-size: 14px; +} + +h4 small { + font-size: 12px; +} + +h5 { + font-size: 12px; +} + +h6 { + font-size: 11px; + color: #999999; + text-transform: uppercase; +} + +.page-header { + padding-bottom: 17px; + margin: 18px 0; + border-bottom: 1px solid #eeeeee; +} + +.page-header h1 { + line-height: 1; +} + +ul, +ol { + padding: 0; + margin: 0 0 9px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +ul { + list-style: disc; +} + +ol { + list-style: decimal; +} + +li { + line-height: 18px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +dl { + margin-bottom: 18px; +} + +dt, +dd { + line-height: 18px; +} + +dt { + font-weight: bold; + line-height: 17px; +} + +dd { + margin-left: 9px; +} + +.dl-horizontal dt { + float: left; + width: 120px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 130px; +} + +hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +.muted { + color: #999999; +} + +abbr[title] { + /* + cursor: help; + border-bottom: 1px dotted #ddd; + */ +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 18px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 22.5px; +} + +blockquote small { + display: block; + line-height: 18px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 18px; + font-style: normal; + line-height: 18px; +} + +small { + font-size: 100%; +} + +cite { + font-style: normal; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 8.5px; + margin: 0 0 9px; + font-size: 12.025px; + line-height: 18px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 18px; +} + +pre code { + padding: 0; + color: inherit; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 18px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 27px; + font-size: 19.5px; + line-height: 36px; + color: #333333; + border: 0; + border-bottom: 1px solid #eee; +} + +legend small { + font-size: 13.5px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 13px; + font-weight: normal; + line-height: 18px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; + color: #333333; +} + +input, +textarea, +select, +.uneditable-input { + display: inline-block; + width: 210px; + height: 18px; + padding: 4px; + margin-bottom: 9px; + font-size: 13px; + line-height: 18px; + color: #555555; + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +label input, +label textarea, +label select { + display: block; +} + +input[type="image"], +input[type="checkbox"], +input[type="radio"] { + width: auto; + height: auto; + padding: 0; + margin: 3px 0; + *margin-top: 0; + /* IE7 */ + + line-height: normal; + cursor: pointer; + background-color: transparent; + border: 0 \9; + /* IE9 and down */ + + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +input[type="image"] { + border: 0; +} + +input[type="file"] { + width: auto; + padding: initial; + line-height: initial; + background-color: #ffffff; + background-color: initial; + border: initial; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +input[type="button"], +input[type="reset"], +input[type="submit"] { + width: auto; + height: auto; +} + +select, +input[type="file"] { + height: 28px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 28px; +} + +input[type="file"] { + line-height: 18px \9; +} + +select { + width: 220px; + background-color: #ffffff; +} + +select[multiple], +select[size] { + height: auto; +} + +input[type="image"] { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +textarea { + height: auto; +} + +input[type="hidden"] { + display: none; +} + +.radio, +.checkbox { + min-height: 18px; + padding-left: 18px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +input, +textarea { + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -ms-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +input:focus, +textarea:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus, +select:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 930px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 850px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 770px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 690px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 610px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 530px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 450px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 370px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 290px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 210px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 130px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 50px; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; + border-color: #ddd; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning > label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; + border-color: #c09853; +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: 0 0 6px #dbc59e; + -moz-box-shadow: 0 0 6px #dbc59e; + box-shadow: 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error > label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; + border-color: #b94a48; +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: 0 0 6px #d59392; + -moz-box-shadow: 0 0 6px #d59392; + box-shadow: 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success > label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; + border-color: #468847; +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: 0 0 6px #7aba7b; + -moz-box-shadow: 0 0 6px #7aba7b; + box-shadow: 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +input:focus:required:invalid, +textarea:focus:required:invalid, +select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:required:invalid:focus, +textarea:focus:required:invalid:focus, +select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 17px 20px 18px; + margin-top: 18px; + margin-bottom: 18px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; + cursor: not-allowed; + background-color: #ffffff; + border-color: #eee; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +:-moz-placeholder { + color: #999999; +} + +::-webkit-input-placeholder { + color: #999999; +} + +.help-block, +.help-inline { + color: #555555; +} + +.help-block { + display: block; + margin-bottom: 9px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-prepend, +.input-append { + margin-bottom: 5px; +} + +.input-prepend input, +.input-append input, +.input-prepend select, +.input-append select, +.input-prepend .uneditable-input, +.input-append .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + vertical-align: middle; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-prepend input:focus, +.input-append input:focus, +.input-prepend select:focus, +.input-append select:focus, +.input-prepend .uneditable-input:focus, +.input-append .uneditable-input:focus { + z-index: 2; +} + +.input-prepend .uneditable-input, +.input-append .uneditable-input { + border-left-color: #ccc; +} + +.input-prepend .add-on, +.input-append .add-on { + display: inline-block; + width: auto; + height: 18px; + min-width: 16px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + vertical-align: middle; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-prepend .add-on, +.input-append .add-on, +.input-prepend .btn, +.input-append .btn { + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend .active, +.input-append .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append .uneditable-input { + border-right-color: #ccc; + border-left-color: #eee; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 14px; + -moz-border-radius: 14px; + border-radius: 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 9px; +} + +legend + .control-group { + margin-top: 18px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 18px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 140px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 160px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 160px; +} + +.form-horizontal .help-block { + margin-top: 9px; + margin-bottom: 0; +} + +.form-horizontal .form-actions { + padding-left: 160px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 18px; +} + +.table th, +.table td { + padding: 8px; + line-height: 18px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapsed; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child th:first-child, +.table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child th:last-child, +.table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child th:first-child, +.table-bordered tbody:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child th:last-child, +.table-bordered tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-striped tbody tr:nth-child(odd) td, +.table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} + +.table tbody tr:hover td, +.table tbody tr:hover th { + background-color: #f5f5f5; +} + +table .span1 { + float: none; + width: 44px; + margin-left: 0; +} + +table .span2 { + float: none; + width: 124px; + margin-left: 0; +} + +table .span3 { + float: none; + width: 204px; + margin-left: 0; +} + +table .span4 { + float: none; + width: 284px; + margin-left: 0; +} + +table .span5 { + float: none; + width: 364px; + margin-left: 0; +} + +table .span6 { + float: none; + width: 444px; + margin-left: 0; +} + +table .span7 { + float: none; + width: 524px; + margin-left: 0; +} + +table .span8 { + float: none; + width: 604px; + margin-left: 0; +} + +table .span9 { + float: none; + width: 684px; + margin-left: 0; +} + +table .span10 { + float: none; + width: 764px; + margin-left: 0; +} + +table .span11 { + float: none; + width: 844px; + margin-left: 0; +} + +table .span12 { + float: none; + width: 924px; + margin-left: 0; +} + +table .span13 { + float: none; + width: 1004px; + margin-left: 0; +} + +table .span14 { + float: none; + width: 1084px; + margin-left: 0; +} + +table .span15 { + float: none; + width: 1164px; + margin-left: 0; +} + +table .span16 { + float: none; + width: 1244px; + margin-left: 0; +} + +table .span17 { + float: none; + width: 1324px; + margin-left: 0; +} + +table .span18 { + float: none; + width: 1404px; + margin-left: 0; +} + +table .span19 { + float: none; + width: 1484px; + margin-left: 0; +} + +table .span20 { + float: none; + width: 1564px; + margin-left: 0; +} + +table .span21 { + float: none; + width: 1644px; + margin-left: 0; +} + +table .span22 { + float: none; + width: 1724px; + margin-left: 0; +} + +table .span23 { + float: none; + width: 1804px; + margin-left: 0; +} + +table .span24 { + float: none; + width: 1884px; + margin-left: 0; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +[class^="icon-"]:last-child, +[class*=" icon-"]:last-child { + *margin-left: 0; +} + +.icon-white { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + background-position: -384px -120px; +} + +.icon-folder-open { + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; + opacity: 0.3; + filter: alpha(opacity=30); +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown:hover .caret, +.open .caret { + opacity: 1; + filter: alpha(opacity=100); +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 4px 0; + margin: 1px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 8px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu a { + display: block; + padding: 3px 15px; + clear: both; + font-weight: normal; + line-height: 18px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu li > a:hover, +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; +} + +.open { + *z-index: 1000; +} + +.open .dropdown-menu { + display: block; +} + +.pull-right .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: "\2191"; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 8px 0; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #eee; + border: 1px solid rgba(0, 0, 0, 0.05); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + filter: alpha(opacity=0); + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -ms-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; + filter: alpha(opacity=100); +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -ms-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 10px 4px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 13px; + line-height: 18px; + *line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(top, #ffffff, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #cccccc; + *border: 0; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + *background-color: #d9d9d9; + /* Buttons in IE7 don't get borders, so darken on hover */ + + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-color: #e6e6e6; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 9px 14px; + font-size: 15px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.btn-large [class^="icon-"] { + margin-top: 1px; +} + +.btn-small { + padding: 5px 9px; + font-size: 11px; + line-height: 16px; +} + +.btn-small [class^="icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding: 2px 6px; + font-size: 11px; + line-height: 14px; +} + +.btn-primary, +.btn-primary:hover, +.btn-warning, +.btn-warning:hover, +.btn-danger, +.btn-danger:hover, +.btn-success, +.btn-success:hover, +.btn-info, +.btn-info:hover, +.btn-inverse, +.btn-inverse:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn { + border-color: #ccc; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} + +.btn-primary { + background-color: #0074cc; + *background-color: #0055cc; + background-image: -ms-linear-gradient(top, #0088cc, #0055cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0055cc); + background-image: -o-linear-gradient(top, #0088cc, #0055cc); + background-image: -moz-linear-gradient(top, #0088cc, #0055cc); + background-image: linear-gradient(top, #0088cc, #0055cc); + background-repeat: repeat-x; + border-color: #0055cc #0055cc #003580; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + background-color: #0055cc; + *background-color: #004ab3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #004099 \9; +} + +.btn-warning { + background-color: #faa732; + *background-color: #f89406; + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + background-color: #da4f49; + *background-color: #bd362f; + background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + background-color: #5bb75b; + *background-color: #51a351; + background-image: -ms-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + background-color: #414141; + *background-color: #222222; + background-image: -ms-linear-gradient(top, #555555, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222)); + background-image: -webkit-linear-gradient(top, #555555, #222222); + background-image: -o-linear-gradient(top, #555555, #222222); + background-image: -moz-linear-gradient(top, #555555, #222222); + background-image: linear-gradient(top, #555555, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 2px; + *padding-bottom: 2px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-group { + position: relative; + *margin-left: .3em; + *zoom: 1; +} + +.btn-group:before, +.btn-group:after { + display: table; + content: ""; +} + +.btn-group:after { + clear: both; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} + +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group > .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .dropdown-toggle { + *padding-top: 4px; + padding-right: 8px; + *padding-bottom: 4px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini.dropdown-toggle { + padding-right: 5px; + padding-left: 5px; +} + +.btn-group > .btn-small.dropdown-toggle { + *padding-top: 4px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large.dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0055cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 7px; + margin-left: 0; +} + +.btn:hover .caret, +.open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} + +.btn-mini .caret { + margin-top: 5px; +} + +.btn-small .caret { + margin-top: 6px; +} + +.btn-large .caret { + margin-top: 6px; + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.dropup .btn-large .caret { + border-top: 0; + border-bottom: 5px solid #000000; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + color: #c09853; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert-heading { + color: inherit; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 18px; +} + +.alert-succeeded { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger, +.alert-failed { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-incomplete { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.alert-skipped { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 18px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > .pull-right { + float: right; +} + +.nav .nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 18px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; + margin-left: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 8px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 18px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.nav-tabs.nav-stacked > li > a:hover { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.nav-tabs .dropdown-toggle .caret, +.nav-pills .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav-tabs .dropdown-toggle:hover .caret, +.nav-pills .dropdown-toggle:hover .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +.nav-tabs .active .dropdown-toggle .caret, +.nav-pills .active .dropdown-toggle .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.nav > .dropdown.active > a:hover { + color: #000000; + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 18px; + overflow: visible; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; +} + +.navbar { + color: #999999; +} + +.navbar .brand:hover { + text-decoration: none; +} + +.navbar .brand { + display: block; + float: left; + padding: 8px 20px 12px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + line-height: 1; + color: #999999; +} + +.navbar .navbar-text { + margin-bottom: 0; + line-height: 40px; +} + +.navbar .navbar-link { + color: #999999; +} + +.navbar .navbar-link:hover { + color: #ffffff; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn { + margin: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 6px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 6px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + color: #ffffff; + background-color: #626262; + border: 1px solid #151515; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -ms-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-search .search-query:focus, +.navbar-search .search-query.focused { + padding: 5px 10px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; +} + +.navbar .nav > li { + display: block; + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 9px 10px 11px; + line-height: 19px; + color: #999999; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar .btn { + display: inline-block; + padding: 4px 10px 4px; + margin: 5px 5px 6px; + line-height: 18px; +} + +.navbar .btn-group { + padding: 5px 5px 6px; + margin: 0; +} + +.navbar .nav > li > a:hover { + color: #ffffff; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav .active > a, +.navbar .nav .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #222222; +} + +.navbar .divider-vertical { + width: 1px; + height: 40px; + margin: 0 9px; + overflow: hidden; + background-color: #222222; + border-right: 1px solid #333333; +} + +.navbar .nav.pull-right { + margin-right: 0; + margin-left: 10px; +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + background-color: #2c2c2c; + *background-color: #222222; + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-image: -moz-linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + background-color: #222222; + *background-color: #151515; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #080808 \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown .dropdown-toggle .caret, +.navbar .nav li.dropdown.open .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar .nav li.dropdown.active .caret { + opacity: 1; + filter: alpha(opacity=100); +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + background-color: transparent; +} + +.navbar .nav li.dropdown.active > .dropdown-toggle:hover { + color: #ffffff; +} + +.navbar .pull-right .dropdown-menu, +.navbar .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right .dropdown-menu:before, +.navbar .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right .dropdown-menu:after, +.navbar .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.breadcrumb { + padding: 7px 14px; + margin: 0 0 18px; + list-style: none; + background-color: #fbfbfb; + background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); + background-image: linear-gradient(top, #ffffff, #f5f5f5); + background-repeat: repeat-x; + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.breadcrumb li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb .divider { + padding: 0 5px; + color: #999999; +} + +.breadcrumb .active a { + color: #333333; +} + +.pagination { + height: 36px; + margin: 18px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination li { + display: inline; +} + +.pagination a { + float: left; + padding: 0 14px; + line-height: 34px; + text-decoration: none; + border: 1px solid #ddd; + border-left-width: 0; +} + +.pagination a:hover, +.pagination .active a { + background-color: #f5f5f5; +} + +.pagination .active a { + color: #999999; + cursor: default; +} + +.pagination .disabled span, +.pagination .disabled a, +.pagination .disabled a:hover { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination li:first-child a { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.pagination li:last-child a { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pager { + margin-bottom: 18px; + margin-left: 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager a { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next a { + float: right; +} + +.pager .previous a { + float: left; +} + +.pager .disabled a, +.pager .disabled a:hover { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-open .dropdown-menu { + z-index: 2050; +} + +.modal-open .dropdown.open { + *z-index: 2050; +} + +.modal-open .popover { + z-index: 2060; +} + +.modal-open .tooltip { + z-index: 2070; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + width: 560px; + margin: -250px 0 0 -280px; + overflow: auto; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -ms-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 50%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-body { + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.tooltip { + position: absolute; + z-index: 1020; + display: block; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + margin-top: -2px; +} + +.tooltip.right { + margin-left: 2px; +} + +.tooltip.bottom { + margin-top: 2px; +} + +.tooltip.left { + margin-left: -2px; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top: 5px solid #000000; + border-right: 5px solid transparent; + border-left: 5px solid transparent; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; + border-left: 5px solid transparent; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-right: 5px solid #000000; + border-bottom: 5px solid transparent; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + padding: 5px; +} + +.popover.top { + margin-top: -5px; +} + +.popover.right { + margin-left: 5px; +} + +.popover.bottom { + margin-top: 5px; +} + +.popover.left { + margin-left: -5px; +} + +.popover.top .arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top: 5px solid #000000; + border-right: 5px solid transparent; + border-left: 5px solid transparent; +} + +.popover.right .arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-right: 5px solid #000000; + border-bottom: 5px solid transparent; +} + +.popover.bottom .arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; + border-left: 5px solid transparent; +} + +.popover.left .arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} + +.popover .arrow { + position: absolute; + width: 0; + height: 0; +} + +.popover-inner { + width: 280px; + padding: 3px; + overflow: hidden; + background: #000000; + background: rgba(0, 0, 0, 0.8); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); +} + +.popover-title { + padding: 9px 15px; + line-height: 1; + background-color: #f5f5f5; + border-bottom: 1px solid #eee; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} + +.popover-content { + padding: 14px; + background-color: #ffffff; + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.popover-content p, +.popover-content ul, +.popover-content ol { + margin-bottom: 0; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 18px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 1; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); +} + +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; +} + +.label, +.badge { + font-size: 10.998px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + padding: 1px 4px 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding: 1px 9px 2px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +a.label:hover, +a.badge:hover { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-failed, +.badge-important { + background-color: #b94a48; +} + +.label-failed[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-incomplete, +.badge-warning { + background-color: #f89406; +} + +.label-incomplete[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-succeeded, +.badge-success { + background-color: #468847; +} + +.label-succeeded[href], +.badge-success[href] { + background-color: #356635; +} + +.label-skipped, +.badge-info { + background-color: #3a87ad; +} + +.label-skipped[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 18px; + margin-bottom: 9px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(top, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + width: 0; + height: 18px; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(top, #149bdf, #0480be); + background-image: -ms-linear-gradient(top, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -ms-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -ms-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(top, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); +} + +.progress-success.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(top, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); +} + +.progress-warning.progress-striped .bar { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 18px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 18px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -ms-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel .item > img { + display: block; + line-height: 1; +} + +.carousel .active, +.carousel .next, +.carousel .prev { + display: block; +} + +.carousel .active { + left: 0; +} + +.carousel .next, +.carousel .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel .next { + left: 100%; +} + +.carousel .prev { + left: -100%; +} + +.carousel .next.left, +.carousel .prev.right { + left: 0; +} + +.carousel .active.left { + left: -100%; +} + +.carousel .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 10px 15px 5px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + color: #ffffff; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 27px; + color: inherit; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.file-selector { + height: 330px; + overflow: scroll; + margin-bottom: 9px; +} + +.file-selector ul { + list-style: none; +} + +.directory, +.file { + width: 125%; +} + +.loader { + text-align: center; + margin-top: 20%; +} + +.test-options { + display: inline-block; + margin-left: 10px; + width: 80px; +} + +.options-description { + display: inline-block; + width: 140px; +} + +.display-description { + display: inline-block; + width: 80px; +} + +.test-display { + display: inline-block; + width: 150px; +} + +label.checkbox { + display: inline-block; + margin-right: 5px; +} + +.centered { + text-align: center; +} + +.notification-message, +.test-message, +.error-message { + margin-top: 10px; +} + +.suite-status { + margin-bottom: 10px; +} + +.test-details, +.error-details { + margin-top: 5px; +} + +.statistics { + margin-bottom: 20px; +} diff --git a/vpu/app/public/img/ajax-loader.gif b/vpu/app/public/img/ajax-loader.gif new file mode 100644 index 0000000..af30cf7 Binary files /dev/null and b/vpu/app/public/img/ajax-loader.gif differ diff --git a/vpu/app/public/img/glyphicons-halflings-white.png b/vpu/app/public/img/glyphicons-halflings-white.png new file mode 100644 index 0000000..79bc568 Binary files /dev/null and b/vpu/app/public/img/glyphicons-halflings-white.png differ diff --git a/vpu/app/public/img/glyphicons-halflings.png b/vpu/app/public/img/glyphicons-halflings.png new file mode 100644 index 0000000..79bc568 Binary files /dev/null and b/vpu/app/public/img/glyphicons-halflings.png differ diff --git a/vpu/app/public/index.php b/vpu/app/public/index.php new file mode 100644 index 0000000..97275d1 --- /dev/null +++ b/vpu/app/public/index.php @@ -0,0 +1,7 @@ +handle($request, \app\config\Routes::get_routes()); +?> diff --git a/vpu/app/public/js/bootstrap-alert.js b/vpu/app/public/js/bootstrap-alert.js new file mode 100644 index 0000000..fc51b5d --- /dev/null +++ b/vpu/app/public/js/bootstrap-alert.js @@ -0,0 +1,90 @@ +/* ========================================================== + * bootstrap-alert.js v2.0.4 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT DATA-API + * ============== */ + + $(function () { + $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) + }) + +}(window.jQuery); diff --git a/vpu/app/public/js/jquery.hotkeys.js b/vpu/app/public/js/jquery.hotkeys.js new file mode 100644 index 0000000..60099e9 --- /dev/null +++ b/vpu/app/public/js/jquery.hotkeys.js @@ -0,0 +1,12 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ +(function(a){function b(b){var c=b.handler,d=(b.namespace||"").toLowerCase().split(" ");d=a.map(d,function(a){return a.split(".")});if(d.length===1&&(d[0]===""||d[0]==="autocomplete")){return}b.handler=function(b){if(this!==b.target&&(/textarea|select/i.test(b.target.nodeName)||b.target.type==="text"||$(b.target).prop("contenteditable")=="true")){return}var e=b.type!=="keypress"&&a.hotkeys.specialKeys[b.which],f=String.fromCharCode(b.which).toLowerCase(),g,h="",i={};if(b.altKey&&e!=="alt"){h+="alt_"}if(b.ctrlKey&&e!=="ctrl"){h+="ctrl_"}if(b.metaKey&&!b.ctrlKey&&e!=="meta"){h+="meta_"}if(b.shiftKey&&e!=="shift"){h+="shift_"}if(e){i[h+e]=true}else{i[h+f]=true;i[h+a.hotkeys.shiftNums[f]]=true;if(h==="shift_"){i[a.hotkeys.shiftNums[f]]=true}}for(var j=0,k=d.length;j","/":"?","\\":"|"}};a.each(["keydown","keyup","keypress"],function(){a.event.special[this]={add:b}})})(jQuery) diff --git a/vpu/app/public/js/jquery.sortElements.js b/vpu/app/public/js/jquery.sortElements.js new file mode 100644 index 0000000..f1ea1d3 --- /dev/null +++ b/vpu/app/public/js/jquery.sortElements.js @@ -0,0 +1,25 @@ +/** + * jQuery.fn.sortElements + * -------------- + * @author James Padolsey (http://james.padolsey.com) + * @version 0.11 + * @updated 18-MAR-2010 + * -------------- + * @param Function comparator: + * Exactly the same behaviour as [1,2,3].sort(comparator) + * + * @param Function getSortable + * A function that should return the element that is + * to be sorted. The comparator will run on the + * current collection, but you may want the actual + * resulting sort to occur on a parent or another + * associated element. + * + * E.g. $('td').sortElements(comparator, function(){ + * return this.parentNode; + * }) + * + * The 's parent () will be sorted instead + * of the itself. + */ +jQuery.fn.sortElements=(function(){var a=[].sort;return function(c,d){d=d||function(){return this};var b=this.map(function(){var f=d.call(this),e=f.parentNode,g=e.insertBefore(document.createTextNode(""),f.nextSibling);return function(){if(e===this){throw new Error("You can't sort elements if any one is a descendant of another.")}e.insertBefore(this,g);e.removeChild(g)}});return a.call(this,c).each(function(e){b[e].call(d.call(this))})}})(); diff --git a/vpu/app/public/js/jqueryFileSelector.js b/vpu/app/public/js/jqueryFileSelector.js new file mode 100644 index 0000000..d900e93 --- /dev/null +++ b/vpu/app/public/js/jqueryFileSelector.js @@ -0,0 +1,111 @@ +(function($) { + + $.extend($.fn, { + fileSelector: function(options) { + options = $.extend({ + callback: function() {}, + collapseSpeed: 500, + expandSpeed: 500, + roots: ['/'], + serverEndpoint: '/' + }, options); + + return this.each(function() { + + function buildTree($fileSelector, dir, isActive) { + $.get(options.serverEndpoint, {dir: dir}, function(response) { + var classAttr = ( $.inArray(dir, options.roots) ) ? " nav" : '', + html = "'; + var $ul = $(html); + $fileSelector.append($ul); + + if ( $.inArray(dir, options.roots) ) { + $fileSelector.find('ul:hidden').show(); + } else { + $fileSelector.find('ul:hidden').slideDown(options.expandSpeed); + } + + bindTree($ul); + }); + } + + function bindTree($fileSelector) { + $fileSelector.find('li a').bind('click', function(event) { + var $this = $(this), + $parent = $this.parent(), + $children = $this.children(), + selector, + $nearest; + + event.preventDefault(); + + if ( $parent.hasClass('directory') ) { + if ( event.metaKey || event.ctrlKey ) { + $parent.toggleClass('active'); + $parent.find('li').toggleClass('active'); + options.callback($this.attr('data-path')); + } else { + if ( $children.hasClass('icon-folder-close') ) { + $parent.find('ul').remove(); + buildTree( + $parent, + encodeURIComponent($this.attr('data-path')), + $parent.hasClass('active') + ); + $children.removeClass().addClass('icon-folder-open'); + } else { + $parent.find('ul').slideUp(options.collapseSpeed); + $children.removeClass().addClass('icon-folder-close'); + } + } + } else { + if ( event.shiftKey ) { + selector = ( $parent.hasClass('active') ) + ? ':not(.active)' + : '.active'; + + if ( $nearest = $parent.siblings(selector) ) { + if ( $nearest.index() > $parent.index() ) { + $parent.nextUntil(selector).toggleClass('active'); + } else { + $parent.prevUntil(selector).toggleClass('active'); + } + } + } + + $parent.toggleClass('active'); + options.callback($this.attr('data-path')); + } + + }); + } + + var length = options.roots.length; + var $self = $(this); + for ( var i = 0; i < length; i++ ) { + buildTree($self, encodeURIComponent(options.roots[i])); + } + }); + } + }); + +})(jQuery); diff --git a/vpu/app/resource/migration/01_CreateSchema.sql b/vpu/app/resource/migration/01_CreateSchema.sql new file mode 100644 index 0000000..cd618ea --- /dev/null +++ b/vpu/app/resource/migration/01_CreateSchema.sql @@ -0,0 +1,22 @@ +CREATE DATABASE `vpu` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +USE `vpu`; + +CREATE TABLE `SuiteResult` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `run_date` datetime NOT NULL, + `failed` int(11) unsigned NOT NULL, + `incomplete` int(11) unsigned NOT NULL, + `skipped` int(11) unsigned NOT NULL, + `succeeded` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `TestResult` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `run_date` datetime NOT NULL, + `failed` int(11) unsigned NOT NULL, + `incomplete` int(11) unsigned NOT NULL, + `skipped` int(11) unsigned NOT NULL, + `succeeded` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/vpu/app/test/DateTest.php b/vpu/app/test/DateTest.php new file mode 100644 index 0000000..626e038 --- /dev/null +++ b/vpu/app/test/DateTest.php @@ -0,0 +1,19 @@ +assertEquals($key, $value, 'test_this() failed!'); + } + + public function test_this_too() { + $key = 'test'; + $value = 'test'; + print_r('foo { breaks: this } bar'); + print_r('foo breaks: this { bar'); + $this->assertEquals($key, $value, 'test_this_too() failed!'); + } +} + +?> diff --git a/vpu/app/test/IncompleteTest.php b/vpu/app/test/IncompleteTest.php new file mode 100644 index 0000000..be9fcc7 --- /dev/null +++ b/vpu/app/test/IncompleteTest.php @@ -0,0 +1,20 @@ +assertTrue(TRUE, 'This should already work.'); + + // Stop here and mark this test as incomplete. + $this->markTestIncomplete('This test has not been implemented yet.'); + } + + public function test_something_else() { + $key = 'test'; + $value = 'test'; + print_r('some stuff'); + $this->assertEquals($key, $value, 'test_something_else() failed!'); + } +} + +?> diff --git a/vpu/app/test/PUTest.php b/vpu/app/test/PUTest.php new file mode 100644 index 0000000..da9a854 --- /dev/null +++ b/vpu/app/test/PUTest.php @@ -0,0 +1,19 @@ +assertEquals($key, $value, 'test_this() failed!'); + } + + public function test_this_too() { + somestr; + $key = 'test'; + $value = 'value'; + $this->assertEquals($key, $value, 'test_this_too() failed!'); + } +} + +?> diff --git a/vpu/app/test/PUTest2.php b/vpu/app/test/PUTest2.php new file mode 100644 index 0000000..f30acb2 --- /dev/null +++ b/vpu/app/test/PUTest2.php @@ -0,0 +1,19 @@ +assertEquals($key, $value, 'test_this() failed!'); + } + + public function test_this_too() { + $key = 'test'; + $value = 'test'; + print_r('some stuff'); + $this->assertEquals($key, $value, 'test_this_too() failed!'); + } +} + +?> diff --git a/vpu/app/test/SkippedTest.php b/vpu/app/test/SkippedTest.php new file mode 100644 index 0000000..8c95206 --- /dev/null +++ b/vpu/app/test/SkippedTest.php @@ -0,0 +1,18 @@ +markTestSkipped('The something_bogus extension is not available.'); + } + } + + public function test_something_else() { + $key = 'test'; + $value = 'value'; + print_r('some stuff'); + $this->assertEquals($key, $value, 'test_something_else() failed!'); + } +} + +?> diff --git a/vpu/app/test/sample_dir/IncompleteTest2.php b/vpu/app/test/sample_dir/IncompleteTest2.php new file mode 100644 index 0000000..fc9a1ab --- /dev/null +++ b/vpu/app/test/sample_dir/IncompleteTest2.php @@ -0,0 +1,20 @@ +assertTrue(TRUE, 'This should already work.'); + + // Stop here and mark this test as incomplete. + $this->markTestIncomplete('This test has not been implemented yet.'); + } + + public function test_something_else() { + $key = 'test'; + $value = 'test'; + print_r('some stuff'); + $this->assertEquals($key, $value, 'test_something_else() failed!'); + } +} + +?> diff --git a/vpu/app/test/sample_dir/PUTest3.php b/vpu/app/test/sample_dir/PUTest3.php new file mode 100644 index 0000000..8e8de9c --- /dev/null +++ b/vpu/app/test/sample_dir/PUTest3.php @@ -0,0 +1,19 @@ +assertEquals($key, $value, 'test_this() failed!'); + } + + public function test_this_too() { + $key = 'test'; + $value = 'test'; + print_r('some stuff'); + $this->assertEquals($key, $value, 'test_this_too() failed!'); + } +} + +?> diff --git a/vpu/app/test/sample_dir/PUTest4.php b/vpu/app/test/sample_dir/PUTest4.php new file mode 100644 index 0000000..b46c4af --- /dev/null +++ b/vpu/app/test/sample_dir/PUTest4.php @@ -0,0 +1,19 @@ +assertEquals($key, $value, 'test_this() failed!'); + } + + public function test_this_too() { + somestr; + $key = 'test'; + $value = 'value'; + $this->assertEquals($key, $value, 'test_this_too() failed!'); + } +} + +?> diff --git a/vpu/app/view/archives/index.html b/vpu/app/view/archives/index.html new file mode 100644 index 0000000..f9d7900 --- /dev/null +++ b/vpu/app/view/archives/index.html @@ -0,0 +1,190 @@ + + + + + VisualPHPUnit - Archives + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + + + + diff --git a/vpu/app/view/graph/index.html b/vpu/app/view/graph/index.html new file mode 100644 index 0000000..228dad4 --- /dev/null +++ b/vpu/app/view/graph/index.html @@ -0,0 +1,198 @@ + + + + + VisualPHPUnit - Graphs + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + diff --git a/vpu/app/view/home/help.html b/vpu/app/view/home/help.html new file mode 100644 index 0000000..da1355b --- /dev/null +++ b/vpu/app/view/home/help.html @@ -0,0 +1,85 @@ + + + + + VisualPHPUnit - Help + + + + + + + + + + + + + + + +
+ +
+
+

About

+

+ VisualPHPUnit was built and is actively maintained by Nick Sinopoli. It is offered under the BSD License. +

+

+ The following tools were indispensable during development: +

+ +

+ Current stable release is v2.2, last updated on May 11, 2013. +

+
+
+

Contributing

+

+ Bug reports and pull requests are welcomed on the project's issue tracker. +

+
+
+

Support

+

+ If you encounter any issues, you may contact NSinopoli@gmail.com for support. +

+
+
+
+ + + diff --git a/vpu/app/view/home/index.html b/vpu/app/view/home/index.html new file mode 100644 index 0000000..c0ab793 --- /dev/null +++ b/vpu/app/view/home/index.html @@ -0,0 +1,418 @@ + + + + + VisualPHPUnit - Home + + + + + + + + + + + + + + + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + diff --git a/vpu/app/view/partial/test_results.html b/vpu/app/view/partial/test_results.html new file mode 100644 index 0000000..6de5390 --- /dev/null +++ b/vpu/app/view/partial/test_results.html @@ -0,0 +1,113 @@ + $suite ): ?> + +
+

+
+ +
+ + +
+

+ +

+ +

+ + +
+ +
+ + + +
+
+

Suite Statistics

+ +

Failed (/)

+
+
+
+ +

Incomplete (/)

+
+
+
+ +

Skipped (/)

+
+
+
+ +

Succeeded (/)

+
+
+
+
+ +
+

Test Statistics

+ +

Failed (/)

+
+
+
+ +

Incomplete (/)

+
+
+
+ +

Skipped (/)

+
+
+
+ +

Succeeded (/)

+
+
+
+ +
+
+ + +

Errors

+ + + +
+

+

+ +

+ +
+ diff --git a/vpu/bin/vpu b/vpu/bin/vpu new file mode 100644 index 0000000..d31cb7b --- /dev/null +++ b/vpu/bin/vpu @@ -0,0 +1,117 @@ +#!/usr/bin/env php + 'xml_configuration_file:', + 'd::' => 'snapshot_directory::', + 'e::' => 'sandbox_errors::', + 's::' => 'store_statistics::' + ); + $options = getopt(implode(array_keys($opts)), array_values($opts)); + + if ( isset($options['f']) ) { + $xml_config = $options['f']; + } elseif ( isset($options['xml_configuration_file']) ) { + $xml_config = $options['xml_configuration_file']; + } else { + $xml_config = false; + } + if ( !$xml_config || !$xml_config = realpath($xml_config) ) { + die( + "A valid xml_configuration_file must be specified " + . "[-f|--xml_configuration_file] for VPU to work.\n" + ); + } + + $vpu = new \app\lib\VPU(); + + if ( isset($options['e']) || isset($options['sandbox_errors']) ) { + $sandbox_errors = true; + } else { + $sandbox_errors = \app\lib\Library::retrieve('sandbox_errors'); + } + if ( $sandbox_errors ) { + error_reporting(\app\lib\Library::retrieve('error_reporting')); + set_error_handler(array($vpu, 'handle_errors')); + } + + $results = $vpu->run_with_xml($xml_config); + $results = $vpu->compile_suites($results, 'cli'); + + if ( $sandbox_errors ) { + restore_error_handler(); + } + + $suites = $results['suites']; + $stats = $results['stats']; + $errors = $vpu->get_errors(); + $to_view = compact('suites', 'stats', 'errors'); + + if ( isset($options['d']) ) { + $filename = $options['d']; + $snapshot_error = '[-d]'; + } elseif ( isset($options['snapshot_directory']) ) { + $filename = $options['snapshot_directory']; + $snapshot_error = '[--snapshot_directory]'; + } else { + $filename = \app\lib\Library::retrieve('snapshot_directory'); + $snapshot_error = 'the snapshot_directory in app/config/bootstrap.php'; + } + $filename = realpath($filename) . '/' . date('Y-m-d_H-i') . '.html'; + + $handle = @fopen($filename, 'a'); + if ( !$handle ) { + die( + "There was an error creating the snapshot. Please ensure that " + . "{$snapshot_error} exists and has the proper permissions.\n" + ); + } + + $view = new \app\core\View(); + $contents = $view->render('partial/test_results', $to_view); + + fwrite($handle, $contents); + fclose($handle); + + echo "Snapshot successfully created at {$filename}.\n"; + + if ( isset($options['s']) || isset($options['store_statistics']) ) { + $store_statistics = true; + } else { + $store_statistics = \app\lib\Library::retrieve('store_statistics'); + } + if ( !$store_statistics ) { + exit; + } + + $db_options = \app\lib\Library::retrieve('db'); + $db = new $db_options['plugin'](); + if ( !$db->connect($db_options) ) { + die( + "There was an error connecting to the database:\n" + . implode(' ', $db->get_errors()) . "\n" + ); + } + + $now = date('Y-m-d H:i:s'); + foreach ( $stats as $key => $stat ) { + $data = array( + 'run_date' => $now, + 'failed' => $stat['failed'], + 'incomplete' => $stat['incomplete'], + 'skipped' => $stat['skipped'], + 'succeeded' => $stat['succeeded'] + ); + $table = ucfirst(rtrim($key, 's')) . 'Result'; + if ( !$db->insert($table, $data) ) { + die( + "There was an error inserting a record into the database:\n" + . implode(' ', $db->get_errors()) . "\n" + ); + } + } + + echo "The statistics generated during this test run were successfully " + . "stored.\n"; +?> diff --git a/vpu/nx/CHANGELOG.md b/vpu/nx/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/vpu/nx/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/vpu/nx/LICENSE b/vpu/nx/LICENSE new file mode 100644 index 0000000..6d0a121 --- /dev/null +++ b/vpu/nx/LICENSE @@ -0,0 +1,33 @@ +NX + +Copyright (c) 2011, Nick Sinopoli . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Nick Sinopoli nor the names of his + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/vpu/nx/README.md b/vpu/nx/README.md new file mode 100644 index 0000000..109620e --- /dev/null +++ b/vpu/nx/README.md @@ -0,0 +1,603 @@ +# NX + +## Table of Contents + +- [Philosophy](#philosophy) +- [Statistics](#statistics) +- [Getting Started](#getting-started) + * [Installing](#installing) + * [Configuring Your Hosts File](#confhosts) + * [Configuring Your Web Server](#confserver) ([nginx](#nginx), or [Apache](#apache)) + * [Restart Your Web Server](#restartserver) +- [Tutorial](#tutorial) +- [API](#api) + * [Request](#request) + * [Response](#response) + * [Dispatcher](#dispatcher) + * [Router](#router) +- [NXtra](#nxtra) +- [Contributing](#contributing) +- [Credits](#credits) +- [License](#license) + +## Philosophy + +> Il semble que la perfection soit atteinte non quand il n'y a plus rien à ajouter, mais quand il n'y a plus rien à retrancher. + +> -- Antoine de Saint-Exupéry + +[PHP](http://codeigniter.com/) [suffers](http://framework.zend.com/) [from](http://symfony.com/) [a](http://www.yiiframework.com/) [plethora](http://cakephp.org/) [of](http://lithify.me/) [frameworks](http://bcosca.github.com/fatfree/). So why another one? + +Rather than offer a "full stack" solution, NX focuses on only that which is _absolutely essential_. All web applications need to handle incoming requests and serve well-formed responses. To that end, NX provides a simple, lightweight solution. Because it leaves out what are essentially _application-specific components_ (ORMs, MV* patterns, form helpers, template engines, etc.), it's also blazingly fast. More importantly, it offers a rock-solid foundation that _never gets in your way_. Best of all, the few components NX does implement are completely modular and easily replaceable. + +## Statistics + +``` +Lines of Code (LOC): 624 + Cyclomatic Complexity / Lines of Code: 0.14 +Comment Lines of Code (CLOC): 256 +Non-Comment Lines of Code (NCLOC): 368 + +Namespaces: 1 +Interfaces: 0 +Classes: 4 + Abstract: 0 (0.00%) + Concrete: 4 (100.00%) + Average Class Length (NCLOC): 89 +Methods: 11 + Scope: + Non-Static: 11 (100.00%) + Static: 0 (0.00%) + Visibility: + Public: 5 (45.45%) + Non-Public: 6 (54.55%) + Average Method Length (NCLOC): 32 + Cyclomatic Complexity / Number of Methods: 5.73 + +Anonymous Functions: 0 +Functions: 0 + +Constants: 0 + Global constants: 0 + Class constants: 0 +``` + +## Getting Started + +### Installing + +```bash +# create a directory for your project +mkdir project && cd project +# install NX and a basic application template +curl -L https://raw.github.com/NSinopoli/nxtra/master/resource/script/install-nx.sh | sh +``` + +### Configuring Your Hosts File + +Choose a server name for your project, and edit your /etc/hosts file accordingly: + +``` +127.0.0.1 project +``` + +### Configuring Your Web Server + +#### nginx + +Place this code block within the `http {}` block in your nginx.conf file: + +```nginx + + server { + server_name project; + root /srv/http/project/app/public; + index index.php; + + access_log /var/log/nginx/project_access.log; + error_log /var/log/nginx/project_error.log; + + location / { + try_files $uri /index.php; + } + + location ~ \.php$ { + fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + } +``` + +Note that you will have to change the `server_name` to the name you used above in your hosts file. You will also have to adjust the directories according to where you checked out the code. In this configuration, /srv/http/project/ is the project root. The public-facing part of your application, on the other hand, is located in app/public within the project root (so in this example, it's /srv/http/project/app/public). + +What's happening here, exactly? The `try_files` directive will check to see if the resouce at $uri exists in the filesystem (in this example, within /srv/http/project/app/public). If it does, that file is served by nginx. If it doesn't, it's then routed to /index.php, whereupon the framework takes responsibility for handling the request. The `try_files` directive is great for serving static content - there's no need to pass requests for js, css, or image files through the framework. + +#### Apache + +In your httpd.conf file, locate your `DocumentRoot`. It will look something like this: + +```apache +DocumentRoot "/srv/http" +``` + +Now find the `` tag that corresponds to your `DocumentRoot`. It will look like this: + +```apache + +``` + +Within that tag, change the `AllowOverride` setting: + +```apache +AllowOverride All +``` + +Ensure that your `DirectoryIndex` setting contains index.php: + +```apache +DirectoryIndex index.php +``` + +Now uncomment the following line: + +```apache +Include conf/extra/httpd-vhosts.conf +``` + +Edit your conf/extra/httpd-vhosts.conf file and add the following code block: + +```apache + + DocumentRoot "/srv/http/project/app/public" + ServerName project + ErrorLog "/var/log/httpd/project_error.log" + CustomLog "/var/log/httpd/project_access.log" common + + Options +FollowSymLinks + + +``` + +Note that you will have to change the `ServerName` to the name you used above in your hosts file. You will also have to adjust the directories ( in `DocumentRoot`, as well as the `` tag) according to where you checked out the code. In this configuration, /srv/http/project/ is the project root. The public-facing part of your application, on the other hand, is located in app/public within the project root (so in this example, it's /srv/http/project/app/public). + +Within your project's public root, create an .htaccess file (in our case, it'd be located at /srv/http/project/app/public/.htaccess) and paste the following block inside: + +```apache + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !favicon.ico$ + RewriteRule ^(.*)$ index.php [QSA,L] + +``` + +### Restart Your Web Server + +Restart your web server, and then point your browser at the server name you chose above. If you see the familiar "Hello, World!", then you've configured everything correctly! + + +## Tutorial + +TODO + + +## API + + +### Request + +The Request class is responsible for organizing all data pertaining to an incoming HTTP request. + +#### Public Properties + +```php +data; + +// Get all of the GET data as an array +$query = $request->query; + +// Get the parameters collected from the request uri +$params = $request->params; + +// Get the request uri +$url = $request->url; + +?> +``` + +#### Request Parameters + +```php +params['id'] + +$routes = array( + array('delete', '/entry/[i:id]', function($request) { + return "Are you sure you want to delete entry {$request->params['id']}?"; + }) +); + +?> +``` + +#### Environment Variables + +All variables collected from PHP's superglobals $_SERVER and $_ENV are available as case-insensitive public properties. + +```php +php_self; +// Get the HTTP_USER_AGENT +$user_agent = $request->http_user_agent; + +?> +``` + +See PHP's documentation on [$_SERVER](http://www.php.net/manual/en/reserved.variables.server.php) and [$_ENV](http://www.php.net/manual/en/reserved.variables.environment.php) to see which data is available. + +#### Checking Request Characteristics +Sometimes it's useful to know what sort of request you're dealing with: + +```php +is('delete') ) { + // do something +} + +// Are we dealing with a mobile user? +if ( $request->is('mobile') ) { + // do something +} + +?> +``` + +The full list of request characteristics is as follows: + +``` +'ajax' - xhr +'delete' - DELETE REQUEST_METHOD +'flash' - "Shockwave Flash" HTTP_USER_AGENT +'get' - GET REQUEST_METHOD +'head' - HEAD REQUEST_METHOD +'mobile' - any one of the following HTTP_USER_AGENTS: + 'Android', 'AvantGo', 'Blackberry', 'DoCoMo', 'iPod', + 'iPhone', 'J2ME', 'NetFront', 'Nokia', 'MIDP', 'Opera Mini', + 'PalmOS', 'PalmSource', 'Plucker', 'portalmmm', + 'ReqwirelessWeb', 'SonyEricsson', 'Symbian', 'UP.Browser', + 'Windows CE', 'Xiino' +'options' - OPTIONS REQUEST_METHOD +'post' - POST REQUEST_METHOD +'put' - PUT REQUEST_METHOD +'ssl' - HTTPS +``` + + +#### Staying RESTful + +Because HTML forms don't support PUT or DELETE, you can use a hidden field in your form (named "_method") to manually override the request method. + +```html +
+ + + +
+``` + +You can then match the request as normal: + +```php +data['name']); + }) +); + +?> +``` + + +### Response + +The Response class is used to output an HTTP response. It consists of three parts: a status code, HTTP headers, and a body. + +#### Rendering a Response + +```php +render($result); + +?> +``` + +```php + "Goodbye, World!", + 'status' => 410, + 'headers' => array('Last-Modified: Tue, 24 Apr 2012 12:45:26 GMT') +); +$response = new \nx\core\Response(); +$response->render($result); + +?> +``` + +##### Status Code + +The status code should be an integer associated with the HTTP status code. Here's the list of supported status codes: + +``` +100 - Continue +101 - Switching Protocols +200 - OK +201 - Created +202 - Accepted +203 - Non-Authoritative Information +204 - No Content +205 - Reset Content +206 - Partial Content +300 - Multiple Choices +301 - Moved Permanently +302 - Found +303 - See Other +304 - Not Modified +305 - Use Proxy +307 - Temporary Redirect +400 - Bad Request +401 - Unauthorized +402 - Payment Required +403 - Forbidden +404 - Not Found +405 - Method Not Allowed +406 - Not Acceptable +407 - Proxy Authentication Required +408 - Request Time-out +409 - Conflict +410 - Gone +411 - Length Required +412 - Precondition Failed +413 - Request Entity Too Large +414 - Request-URI Too Large +415 - Unsupported Media Type +416 - Requested range not satisfiable +417 - Expectation Failed +500 - Internal Server Error +501 - Not Implemented +502 - Bad Gateway +503 - Service Unavailable +504 - Gateway Time-out +``` + +If a status code is not provided, the response will default to using 200 OK. + +##### Headers + +Headers should be an array of well-formed HTTP headers. See [Wikipedia's entry on HTTP header responses](https://en.wikipedia.org/wiki/HTTP_header#Responses) for more information. If no headers are provided, the response will default to using a "Content-Type: text/html; charset=utf-8" header. + +##### Body + +The body contains the message data. The body is 'chunked' (see [this article](http://wonko.com/post/seeing_poor_performance_using_phps_echo_statement_heres_why) and [this one as well](http://weblog.rubyonrails.org/2011/4/18/why-http-streaming/) for more information) according to the buffer_size set in the constructor (defaults to 8192 bytes). + +```php + +``` + + +### Dispatcher + +The dispatcher is responsible for connecting requests with responses. + +#### Handling an Incoming Request +The dispatcher passes an incoming [Request](#request) (along with predefined routes) to the [Router](#router), whereupon an array is returned containing the parameters collected from the request uri as well as the callback function provided in the matched route. + +#### Rendering a Response +The acquired callback function is called by the dispatcher, whose return value is then passed to the [Response](#response) class for rendering. + + +### Router + +Every incoming request has an associated uri and method. The router's responsibility is to match the request uri and method to a predefined route. + +#### Defining Routes + +##### Construction + +Routes are constructed using the following format: + +```php + +``` + +##### Parameters + +``` +$request_method can be a string (one of 'GET', 'POST', 'PUT', or 'DELETE'), or an array containing a combination of request +methods. Note that these are case-insensitive. +``` +``` +$request_uri is a regex-like pattern, providing support for optional match types. + +Valid match types are as follows: +[i] - integer +[a] - alphanumeric +[h] - hexadecimal +[*] - anything + +Match types can be combined with parameter names, which will be captured: +[i:id] - will match an integer, storing it within the returned 'params' array under the 'id' key +[a:name] - will match an alphanumeric value, storing it within the returned 'params' array under the 'name' key + +Here are some examples to help illustrate: +/post/[i:id] - will match on /post/32 (with the returned 'params' array containing an 'id' key with a value of 32), but will +not match on /post/today +/find/[h:serial] - will match on /find/ae32 (with the returned 'params' array containing a 'serial' key with a value +of 'ae32'), but will not match on /find/john +``` +``` +$callback is a valid callback function, which can take a Request object as its first parameter. The return value should be +a proper Response (i.e., either a string [the body of the response] or an array [containing any combination of body, headers, +and status]). +``` + +##### Notes + +Routes are processed in the order in which they are defined. Because only one route is used as the match, you'll want to define the routes so that they follow each other with decreased specificity (i.e., most specific at the top, most general at the bottom). + +##### Examples + +```php +params['id']}?"; + }), + + // Matches all other GET requests - we can use this to capture 404s + array('get', '*', function($request) { + return array( + 'status' => 404, + 'body' => '

404 Not Found

' + ); + }) +); + +?> +``` + +##### Connecting Application Routes to NX + +Routes are not directly passed to the Router. Instead, they are passed to the [Dispatcher](#dispatcher) (along with a [Request](#request) object): + +```php +handle($request, $routes); + +?> +``` + + +## NXtra + +Several common application libraries were built while developing and using NX. Rather than include them in the core framework, they have been placed into a separate repository. Check out [NXtra](http://git.io/nxtra) for more information. + + +## Contributing + +Please submit any feedback you may have on the project's [issue tracker](https://github.com/NSinopoli/nx/issues). + + +## Credits + +Huge thanks to [Chris O'Hara](https://github.com/chriso), whose [router](https://github.com/chriso/klein.php) was adapted for use in NX. + +Special thanks to [Andrew Ettinger](https://github.com/sillydeveloper), whose [ploof](https://github.com/sillydeveloper/ploof) framework provided the initial inspiration. + + +## License + +NX is licensed under [The BSD License](http://opensource.org/licenses/BSD-3-Clause). + +``` +Copyright (c) 2011-2012, Nick Sinopoli . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Nick Sinopoli nor the names of his + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +``` diff --git a/vpu/nx/core/Dispatcher.php b/vpu/nx/core/Dispatcher.php new file mode 100644 index 0000000..dbe49c9 --- /dev/null +++ b/vpu/nx/core/Dispatcher.php @@ -0,0 +1,62 @@ + + * @copyright 2011-2012 Nick Sinopoli + * @license http://opensource.org/licenses/BSD-3-Clause The BSD License + */ +class Dispatcher { + + /** + * The configuration settings. + * + * @var array + */ + protected $_config = array(); + + /** + * Sets the configuration options for the dispatcher. + * + * @param array $config The configuration options. + * @return void + */ + public function __construct(array $config = array()) { + $defaults = array( + 'response' => new \nx\core\Response(), + 'router' => new \nx\core\Router() + ); + $this->_config = $config + $defaults; + } + + /** + * Matches an incoming request with the supplied routes, calls the + * callback associated with the matched route, and sends a response. + * + * @param object $request The incoming request object. + * @param array $routes The routes. + * @return void + */ + public function handle($request, $routes) { + $method = $request->request_method; + + $router = $this->_config['router']; + $parsed = $router->parse($request->url, $method, $routes); + + if ( $parsed['callback'] ) { + $request->params = $parsed['params']; + $result = call_user_func($parsed['callback'], $request); + } else { + $result = false; + } + + $response = $this->_config['response']; + $response->render($result); + } + +} + +?> diff --git a/vpu/nx/core/Request.php b/vpu/nx/core/Request.php new file mode 100644 index 0000000..841a237 --- /dev/null +++ b/vpu/nx/core/Request.php @@ -0,0 +1,221 @@ + + * @copyright 2011-2012 Nick Sinopoli + * @license http://opensource.org/licenses/BSD-3-Clause The BSD License + */ +class Request { + + /** + * The POST/PUT/DELETE data. + * + * @var array + */ + public $data = array(); + + /** + * The environment variables. + * + * @var array + */ + protected $_env = array(); + + /** + * The GET data. + * + * @var array + */ + public $query = array(); + + /** + * The parameters parsed from the request url. + * + * @var array + */ + public $params; + + /** + * The url of the request. + * + * @var string + */ + public $url; + + /** + * Sets the configuration options. + * + * @param array $config The configuration options. Possible keys + * include: + * 'data' - the POST/PUT/DELETE data + * 'query' - the GET data + * @return void + */ + public function __construct(array $config = array()) { + $defaults = array( + 'data' => array(), + 'query' => array() + ); + + $config += $defaults; + + $this->_env = $_SERVER + $_ENV + array( + 'CONTENT_TYPE' => 'text/html', + 'REQUEST_METHOD' => 'GET' + ); + + if ( isset($this->_env['SCRIPT_URI']) ) { + $this->_env['HTTPS'] = + ( strpos($this->_env['SCRIPT_URI'], 'https://') === 0 ); + } elseif ( isset($this->_env['HTTPS']) ) { + $this->_env['HTTPS'] = ( + !empty($this->_env['HTTPS']) && $this->_env['HTTPS'] !== 'off' + ); + } else { + $this->_env['HTTPS'] = false; + } + + $parsed = parse_url($this->_env['REQUEST_URI']); + + $base = '/' . ltrim( + str_replace('\\', '/', dirname($this->_env['PHP_SELF'])), + '/'); + $base = rtrim(str_replace('/app/public', '', $base), '/'); + $pattern = '/^' . preg_quote($base, '/') . '/'; + $this->url = '/' . trim( + preg_replace($pattern, '', $parsed['path']), + '/'); + + $query = array(); + if ( isset($parsed['query']) ) { + $query_string = str_replace('%20', '+', $parsed['query']); + parse_str(rawurldecode($query_string), $query); + } + $this->query = $config['query'] + $query; + + $this->data = $config['data']; + if ( isset($_POST) ) { + $this->data += $_POST; + } + + $override ='HTTP_X_HTTP_METHOD_OVERRIDE'; + if ( isset($this->data['_method']) ) { + $this->_env[$override] = strtoupper($this->data['_method']); + unset($this->data['_method']); + } + if ( !empty($this->_env[$override]) ) { + $this->_env['REQUEST_METHOD'] = $this->_env[$override]; + } + + $method = strtoupper($this->_env['REQUEST_METHOD']); + + if ( $method == 'PUT' || $method == 'DELETE' ) { + $stream = fopen('php://input', 'r'); + parse_str(stream_get_contents($stream), $this->data); + fclose($stream); + } + + } + + /** + * Returns an environment variable. + * + * @param string $key The environment variable. + * @return mixed + */ + public function __get($key) { + $key = strtoupper($key); + return ( isset($this->_env[$key]) ) ? $this->_env[$key] : null; + } + + /** + * Checks for request characteristics. + * + * The full list of request characteristics is as follows: + * + * * 'ajax' - XHR + * * 'delete' - DELETE REQUEST_METHOD + * * 'flash' - "Shockwave Flash" HTTP_USER_AGENT + * * 'get' - GET REQUEST_METHOD + * * 'head' - HEAD REQUEST_METHOD + * * 'mobile' - any one of the following HTTP_USER_AGENTS: + * + * 1. 'Android' + * 1. 'AvantGo' + * 1. 'Blackberry' + * 1. 'DoCoMo' + * 1. 'iPod' + * 1. 'iPhone' + * 1. 'J2ME' + * 1. 'NetFront' + * 1. 'Nokia' + * 1. 'MIDP' + * 1. 'Opera Mini' + * 1. 'PalmOS' + * 1. 'PalmSource' + * 1. 'Plucker' + * 1. 'portalmmm' + * 1. 'ReqwirelessWeb' + * 1. 'SonyEricsson' + * 1. 'Symbian' + * 1. 'UP.Browser' + * 1. 'Windows CE' + * 1. 'Xiino' + * + * * 'options' - OPTIONS REQUEST_METHOD + * * 'post' - POST REQUEST_METHOD + * * 'put' - PUT REQUEST_METHOD + * * 'ssl' - HTTPS + * + * @param string $characteristic The characteristic. + * @return bool + */ + public function is($characteristic) { + switch ( strtolower($characteristic) ) { + case 'ajax': + return ( + $this->http_x_requested_with == 'XMLHttpRequest' + ); + case 'delete': + return ( $this->request_method == 'DELETE' ); + case 'flash': + return ( + $this->http_user_agent == 'Shockwave Flash' + ); + case 'get': + return ( $this->request_method == 'GET' ); + case 'head': + return ( $this->request_method == 'HEAD' ); + case 'mobile': + $mobile_user_agents = array( + 'Android', 'AvantGo', 'Blackberry', 'DoCoMo', 'iPod', + 'iPhone', 'J2ME', 'NetFront', 'Nokia', 'MIDP', 'Opera Mini', + 'PalmOS', 'PalmSource', 'Plucker', 'portalmmm', + 'ReqwirelessWeb', 'SonyEricsson', 'Symbian', 'UP\.Browser', + 'Windows CE', 'Xiino' + ); + $pattern = '/' . implode('|', $mobile_user_agents) . '/i'; + return (boolean) preg_match( + $pattern, $this->http_user_agent + ); + case 'options': + return ( $this->request_method == 'OPTIONS' ); + case 'post': + return ( $this->request_method == 'POST' ); + case 'put': + return ( $this->request_method == 'PUT' ); + case 'ssl': + return $this->https; + default: + return false; + } + } + +} + +?> diff --git a/vpu/nx/core/Response.php b/vpu/nx/core/Response.php new file mode 100644 index 0000000..e3b7d5c --- /dev/null +++ b/vpu/nx/core/Response.php @@ -0,0 +1,159 @@ + + * @copyright 2011-2012 Nick Sinopoli + * @license http://opensource.org/licenses/BSD-3-Clause The BSD License + */ +class Response { + + /** + * The configuration settings. + * + * @var array + */ + protected $_config = array(); + + /** + * The HTTP status codes. + * + * @var array + */ + protected $_statuses = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out' + ); + + /** + * Sets the configuration options. + * + * Possible keys include the following: + * + * * 'buffer_size' - The number of bytes each chunk of output should contain + * + * @param array $config The configuration options. + * @return void + */ + public function __construct(array $config = array()) { + $defaults = array( + 'buffer_size' => 8192 + ); + $this->_config = $config + $defaults; + } + + /** + * Converts an integer status to a well-formed HTTP status header. + * + * @param int $code The integer associated with the HTTP status. + * @return string + */ + protected function _convert_status($code) { + if ( isset($this->_statuses[$code]) ) { + return "HTTP/1.1 {$code} {$this->_statuses[$code]}"; + } + return "HTTP/1.1 200 OK"; + } + + /** + * Parses a response. + * + * @param mixed $response The response to be parsed. Can be an array + * containing 'body', 'headers', and/or 'status' + * keys, or a string which will be used as the + * body of the response. Note that the headers + * must be well-formed HTTP headers, and the + * status must be an integer (i.e., the one + * associated with the HTTP status code). + * @return array + */ + protected function _parse($response) { + $defaults = array( + 'body' => '', + 'headers' => array('Content-Type: text/html; charset=utf-8'), + 'status' => 200 + ); + if ( is_array($response) ) { + $response += $defaults; + } elseif ( is_string($response) ) { + $defaults['body'] = $response; + $response = $defaults; + } else { + $defaults['status'] = 500; + $response = $defaults; + } + return $response; + } + + /** + * Renders a response. + * + * @param mixed $response The response to be rendered. Can be an array + * containing 'body', 'headers', and/or 'status' + * keys, or a string which will be used as the + * body of the response. Note that the headers + * must be well-formed HTTP headers, and the + * status must be an integer (i.e., the one + * associated with the HTTP status code). The + * response body is chunked according to the + * buffer_size set in the constructor. + * @return void + */ + public function render($response) { + $response = $this->_parse($response); + $status = $this->_convert_status($response['status']); + header($status); + foreach ( $response['headers'] as $header ) { + header($header, false); + } + + $buffer_size = $this->_config['buffer_size']; + $length = strlen($response['body']); + for ( $i = 0; $i < $length; $i += $buffer_size ) { + echo substr($response['body'], $i, $buffer_size); + } + } + +} + +?> diff --git a/vpu/nx/core/Router.php b/vpu/nx/core/Router.php new file mode 100644 index 0000000..6ab5b09 --- /dev/null +++ b/vpu/nx/core/Router.php @@ -0,0 +1,181 @@ + + * @copyright 2011-2012 Nick Sinopoli + * @license http://opensource.org/licenses/BSD-3-Clause The BSD License + */ +class Router { + + /** + * Compiles the regex necessary to capture all match types within a route. + * + * @param string $route The route. + * @return string + */ + protected function _compile_regex($route) { + $pattern = '`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`'; + + if ( preg_match_all($pattern, $route, $matches, PREG_SET_ORDER) ) { + $match_types = array( + 'i' => '[0-9]++', + 'a' => '[0-9A-Za-z]++', + 'h' => '[0-9A-Fa-f]++', + '*' => '.+?', + '' => '[^/]++' + ); + foreach ( $matches as $match ) { + list($block, $pre, $type, $param, $optional) = $match; + + if ( isset($match_types[$type]) ) { + $type = $match_types[$type]; + } + if ( $param ) { + $param = "?<{$param}>"; + } + if ( $optional ) { + $optional = '?'; + } + + $replaced = "(?:{$pre}({$param}{$type})){$optional}"; + $route = str_replace($block, $replaced, $route); + } + } + if ( substr($route, strlen($route) - 1) != '/' ) { + $route .= '/?'; + } + return "`^{$route}$`"; + } + + /** + * Parses the supplied request uri based on the supplied routes and + * the request method. + * + * Routes should be of the following format: + * + * + * $routes = array( + * array( + * mixed $request_method, string $request_uri, callable $callback + * ), + * ... + * ); + * + * + * where: + * + * + * $request_method can be a string ('GET', 'POST', 'PUT', 'DELETE'), + * or an array (e.g., array('GET, 'POST')). Note that $request_method + * is case-insensitive. + * + * + * + * $request_uri is a string, with optional match types. Valid match types + * are as follows: + * + * [i] - integer + * [a] - alphanumeric + * [h] - hexadecimal + * [*] - anything + * + * Match types can be combined with parameter names, which will be + * captured: + * + * [i:id] - will match an integer, storing it within the returned 'params' + * array under the 'id' key + * [a:name] - will match an alphanumeric value, storing it within the + * returned 'params' array under the 'name' key + * + * Here are some examples to help illustrate: + * + * /post/[i:id] - will match on /post/32 (with the returned 'params' array + * containing an 'id' key with a value of 32), but will not match on + * /post/today + * + * /find/[h:serial] - will match on /find/ae32 (with the returned 'params' + * array containing a 'serial' key will a value of 'ae32'), but will not + * match on /find/john + * + * + * + * $callback is a valid callback function. + * + * + * Returns an array containing the following keys: + * + * * 'params' - The parameters collected from the matched uri + * * 'callback' - The callback function pulled from the matched route + * + * @param string $request_uri The request uri. + * @param string $request_method The request method. + * @param array $routes The routes. + * @return array + */ + public function parse($request_uri, $request_method, $routes) { + foreach ( $routes as $route ) { + list($method, $uri, $callback) = $route; + + if ( is_array($method) ) { + $found = false; + foreach ( $method as $value ) { + if ( strcasecmp($request_method, $value) == 0 ) { + $found = true; + break; + } + } + if ( !$found ) { + continue; + } + } elseif ( strcasecmp($request_method, $method) != 0 ) { + continue; + } + + if ( is_null($uri) || $uri == '*' ) { + $params = array(); + return compact('params', 'callback'); + } + + $route_to_match = ''; + $len = strlen($uri); + + for ( $i = 0; $i < $len; $i++ ) { + $char = $uri[$i]; + $is_regex = ( + $char == '[' || $char == '(' || $char == '.' + || $char == '?' || $char == '+' || $char == '{' + ); + if ( $is_regex ) { + $route_to_match = $uri; + break; + } elseif ( + !isset($request_uri[$i]) || $char != $request_uri[$i] + ) { + continue 2; + } + $route_to_match .= $char; + } + + $regex = $this->_compile_regex($route_to_match); + if ( preg_match($regex, $request_uri, $params) ) { + foreach ( $params as $key => $arg ) { + if ( is_numeric($key) ) { + unset($params[$key]); + } + } + return compact('params', 'callback'); + } + } + return array( + 'params' => null, + 'callback' => null + ); + } + +} + +?>