<?php /** * Class to generate a status report of an Web Application. */ class Archimedes { public $fields = array(); public $type; public $author; public $id; public function __construct($type, $author, $id) { $this->type = $type; $this->author = $author; $this->id = $id; } public function toXML() { $this->validate(); $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = TRUE; $node = new DOMElement('node',null,'monitor:node'); $dom->appendChild($node); $node->setAttribute('type',$this->type); $node->setAttribute('id',$this->id); $node->setAttribute('datetime',date('c')); $node->setAttribute('author','mailto:' . $this->author); foreach($this->fields as $field) { $field->compile($node); } return $dom->saveXML(); } /** * Validate the structure of the report. */ protected function validate() { if (!isset($this->id)) { throw new ArchimedesClientException("No ID set."); } if (!isset($this->type)) { throw new ArchimedesClientException("No type defined."); } if (!isset($this->author)) { throw new ArchimedesClientException("No author given."); } if (!isset($this->fields['title'])) { throw new ArchimedesClientException("No title present."); } return TRUE; } public function encrypt($key) { $data = $this->toXML(); $pubkey = openssl_pkey_get_public($key); openssl_seal($data,$sealed,$ekeys,array($pubkey)); openssl_free_key($pubkey); $this->encrypted = $sealed; $this->ekey = $ekeys[0]; return $this; } /** * Encrypt the data. */ protected function getEncrypted() { if (!$this->encrypted) { throw new Exception("Can not retrive encrypted data. Data has not yet been encrypted."); } return $this->encrypted; } public function __toString() { return base64_encode($this->getEncrypted()); } /** * Post the data directly to the Archimedes Server. */ public function postXML($server_url) { // Parse the URL and make sure we can handle the schema. $uri = parse_url($server_url); if ($uri == FALSE) { throw new Exception('Unable to parse URL.'); } if (!isset($uri['scheme'])) { throw new Exception('Missing URL schema for: ['. $uri . ']' ); } switch ($uri['scheme']) { case 'http': $port = isset($uri['port']) ? $uri['port'] : 80; $host = $uri['host'] . ($port != 80 ? ':'. $port : ''); $fp = @fsockopen($uri['host'], $port, $errno, $errstr, 15); break; case 'https': // Note: Only works for PHP 4.3 compiled with OpenSSL. $port = isset($uri['port']) ? $uri['port'] : 443; $host = $uri['host'] . ($port != 443 ? ':'. $port : ''); $fp = @fsockopen('ssl://'. $uri['host'], $port, $errno, $errstr, 20); break; default: throw new Exception('Invalid schema '. $uri['scheme'] . '.'); } // Make sure the socket opened properly. if (!$fp) { throw new Exception(trim($errstr)); } // Construct the path to act on. $path = isset($uri['path']) ? $uri['path'] : '/'; if (isset($uri['query'])) { $path .= '?'. $uri['query']; } $content['data'] = (string) $this; $content['key'] = base64_encode($this->ekey); $content = json_encode($content); // Create HTTP request. $defaults = array( // RFC 2616: "non-standard ports MUST, default ports MAY be included". // We don't add the port to prevent from breaking rewrite rules checking the // host that do not take into account the port number. 'Host' => "Host: $host", 'User-Agent' => 'User-Agent: (Archimedes Client)', ); $defaults['Content-Length'] = 'Content-Length: '. strlen($content); // If the server url has a user then attempt to use basic authentication if (isset($uri['user'])) { $defaults['Authorization'] = 'Authorization: Basic '. base64_encode($uri['user'] . (!empty($uri['pass']) ? ":". $uri['pass'] : '')); } $request = 'POST '. $path ." HTTP/1.0\r\n"; $request .= implode("\r\n", $defaults); $request .= "\r\n\r\n"; $request .= $content; fwrite($fp, $request); // Fetch response. $response = ''; while (!feof($fp) && $chunk = fread($fp, 1024)) { $response .= $chunk; } fclose($fp); // Parse response. list($split, $result) = explode("\r\n\r\n", $response, 2); $split = preg_split("/\r\n|\n|\r/", $split); list($protocol, $code, $text) = explode(' ', trim(array_shift($split)), 3); // Parse headers. while ($line = trim(array_shift($split))) { list($header, $value) = explode(':', $line, 2); $headers[$header] = trim($value); } $code = floor($code / 100) * 100; switch ($code) { case 200: // OK case 304: // Not modified case 301: // Moved permanently case 302: // Moved temporarily case 307: // Moved temporarily break; default: return FALSE; } $status = json_decode($result); return $status->success; } /** * Send the XML report via email. */ public function sendXML($email) { $attachment = chunk_split((string) $this); $site_name = (string) $this->getField('title'); $boundary = '-----=' . md5(uniqid(rand())); $headers = 'From: ' . $site_name . ' <' . $this->author . '>' . "\r\n"; $headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . '"' . "\r\n"; $headers .= 'Mime-Version: 1.0' . "\r\n"; $message = '--' . $boundary . "\r\n"; $message .= "Content-Type: text/plain\r\n"; $message .= "Content-Transfer-Encoding: 7bit\r\n\r\n"; $message .= "Archimedes XML update attached.\r\n"; $message .= '--' . $boundary . "\r\n"; $message .= "Content-Type: text/plain\r\n"; $message .= "Content-Transfer-Encoding: base64\r\n\r\n"; $message .= chunk_split(base64_encode("EKEY: " . $this->ekey)); $message .= '--' . $boundary . "\r\n"; $message .= 'Content-Type: application/xml; name="data.xml"' . "\r\n"; $message .= 'Content-Transfer-Encoding: base64' . "\r\n"; $message .= 'Content-Disposition: attachment; filename="data.xml"' . "\r\n\r\n"; $message .= $attachment . "\r\n"; $message .= '--' . $boundary . "\r\n"; return mail($email, 'XML Update from' . ' ' . $site_name, $message, $headers); } /** * Add a new field to the report. */ public function createField($fieldID, $values = array()) { // Ensure the value is an array. // Strings will be type casted to arrays. $values = (array) $values; $field = new ArchimedesField($fieldID); $this->addField($fieldID,$field); foreach ($values as $value) { $field->addValue($value); } return $field; } protected function addField($fieldID,$field) { $this->fields[$fieldID] = $field; } public function getField($fieldID) { return $this->fields[$fieldID]; } } Class ArchimedesField { public $fieldID; protected $facet = FALSE; protected $type = ''; protected $values = array(); protected $namespace = ''; public function __construct($fieldID) { $this->fieldID = $fieldID; } public function addValue($value) { if (!is_object($value)) { $value = new ANSValue($value); } $this->values[] = $value; return $this; } public function getValues() { return $this->values; } public function invokeFacet() { $this->facet = TRUE; return $this; } public function revokeFacet() { $this->facet = FALSE; return $this; } /** * Compile the field into a DOMElement. */ function compile($node) { $field = new DOMElement('field'); $node->appendChild($field); $field->setAttribute('id',$this->fieldID); foreach($this->values as $value) { $value->compile($field); if ($this->facet) { $value->nodeValue = ''; $value->appendChild(new DOMElement('facet', htmlspecialchars((string) $value))); } } return $field; } public function __toString() { $list = array(); foreach($this->values as $value) { $list[] = (string) $value; } return implode(', ',$list); } public function toArray() { $list = array(); foreach($this->values as $value) { $list[] = $value->toArray(); } return $list; } } Class ANSValue extends DOMElement { // Namespace attributes. protected $ns_attr = array(); protected $ns = null; // Normal attributes. protected $attr = array(); protected $value = ''; public $facet = FALSE; public function __construct($val) { parent::__construct('value', htmlspecialchars($val)); $this->value = $val; } public function setAttribute($name, $value) { $this->attr[$name] = $value; return $this; } public function setAttributeNS($ns, $name, $value) { if (strpos($name, ':') === FALSE) { return $this->setAttribute($name, $value); } $this->ns_attr[$name] = $value; return $this; } public function getAttribute($name) { return $this->attr[$name]; } public function getAttributeNS($name, $local_name) { return $this->ns_attr[$name]; } /** * Append a DOMElement to a parent node. */ public function compile($field) { $field->appendChild($this); foreach ($this->attr as $key => $value) { parent::setAttribute($key, $value); } foreach ($this->ns_attr as $key => $value) { parent::setAttributeNS($this->ns, $key, $value); } return $this; } public function __toString() { return (string) $this->value; } } Class Archimedes_nodereference extends ANSValue { public function __construct($value) { if (!isset($this->ns)) $this->ns = 'monitor-plugin:node'; parent::__construct($value); $this->setAttributeNS($this->ns, 'node:title', $value); } public function addNode(Array $node) { $required_keys = array('title','type'); $keys_diff = array_diff($required_keys, array_keys($node)); if (!empty($keys_diff)) { throw new ArchimedesClientException("Missing required attributes for node reference: " . implode(', ', $keys_diff)); } foreach ($node as $key => $value) { $this->setAttributeNS($this->ns, 'node:' . $key, $value); } return $this; } } Class Archimedes_userreference extends ANSValue { public function __construct($value) { $this->ns = 'monitor-plugin:user'; parent::__construct($value); } public function addUser(Array $user) { $required_keys = array('type'); $keys_diff = array_diff($required_keys, array_keys($user)); if (!empty($keys_diff)) { throw new ArchimedesClientException("Missing required attributes for user reference: " . implode(', ', $keys_diff)); } foreach ($required_keys as $key) { $this->setAttributeNS($this->ns, 'user:' . $key, $user[$key]); } return $this; } } Class Archimedes_drupalmod extends Archimedes_nodereference { public function __construct($value) { $this->ns = 'monitor-plugin:drupal-module'; parent::__construct($value); } public function toArray() { return array('name' => (string) $this->value, 'version' => $this->getAttributeNS('node:field_mod_version'), 'desc' => $this->getAttributeNS('node:body')); } } Class Archimedes_moodlemod extends Archimedes_nodereference { public function __construct($value) { $this->ns = 'monitor-plugin:moodle-module'; parent::__construct($value); } public function toArray() { return array('name' => (string) $this->value, 'version' => $this->getAttributeNS('node:field_mod_version','node:version'), 'instances' => $this->getAttributeNS('node:instances')); } } Class Archimedes_gitrepo extends ANSValue { public function __construct($value) { $this->ns = 'monitor-plugin:git'; parent::__construct($value); } public function setRemoteName($name) { $this->setAttributeNS($this->ns,'git:remote', $name); return $this; } public function toArray() { return array('remote' => $this->getAttributeNS('git:remote'),'uri' => (string) $this->value); } } Class Archimedes_dataset extends ANSValue { public function __construct($value) { $this->ns = 'monitor-plugin:dataset'; parent::__construct($value); } public function setTitle($title) { $this->setAttributeNS($this->ns, 'dataset:title', $title); return $this; } public function toArray() { return array('title' => $this->getAttributeNS('dataset:title'),'value' => (string) $this->value); } } /** * Archimedes Exception Class. */ class ArchimedesClientException extends Exception { } /** * Wrapper function for creating a new value. */ function archimedes_value($value, $type = '') { if (empty($type)) { return new ANSValue($value); } $class = 'Archimedes_' . $type; if (!class_exists($class)) { throw new ArchimedesClientException("No such plugin available for $type"); } return new $class($value); } class ArchimedesRemoteRequest { protected $hash; protected $key; protected $token; /** * @param field_unique_hash * @param public key. */ public function getToken($hash, $key) { $this->hash = $hash; $this->key = $key; foreach (array('h', 't', 'i') as $k) { if (!isset($_GET[$k])) { return FALSE; } } // $_GET['i'] is the unique identifier for this site md5 hashed with the time. // If it doesn't match then its likely this request is forged. If the requester // does know the unique hash of this site then we will trust this request is // not a spammer. if ($_GET['i'] != md5($_GET['t'] . $hash)) { return FALSE; } // Add a random number prefix incase the time here is the same as the time passed // in the original request (cause then the hashes would be the same). return $this->token = md5(mt_rand(1000, 10000) . time()); } public function validateRemoteUser($redirect = FALSE) { if (empty($this->token)) { return FALSE; } if (!$redirect) { $redirect = 'http://' . $_SERVER['SERVER_NAME'] . $_SERVER['REDIRECT_URL']; } $query = array( 'token' => $this->token, 'redirect' => $redirect, 'hash' => $this->hash, ); $pubkey = openssl_pkey_get_public($this->key); openssl_seal(serialize($query),$sealed,$ekeys,array($pubkey)); openssl_free_key($pubkey); $url = 'http://' . $_GET['h'] . '/archimedes-server/verify-user?ekey=' . rawurlencode($ekeys[0]) . '&data=' . rawurlencode($sealed); header("Location: $url"); die; } public function validateToken($local_token) { return $local_token == $_GET['token']; } } function archimedes_directory_hash($dir, $ignore) { // Symlink count is important. While we don't want to follow // symlinks, we need to know they are there incase they are // introduced or removed. $symlinks = array(); if (!is_dir($dir)) { return false; } $filemd5s = array(); $d = dir($dir); while (($entry = $d->read()) !== false) { if (in_array($entry, array('.', '..'))) { continue; } $path = realpath($dir . '/' . $entry); // If the begining of the path does not match exactly then // this directory does not lead deeper but to somewhere else which // may create a recursive loop. if (strpos($path, $dir) !== 0) { $symlinks[] = $path; continue; } // Symlinks may introduce recursive loops. if (is_link($path)) { $symlinks[] = $path; continue; } $ignore_entry = FALSE; foreach($ignore as $pattern) { if(preg_match($pattern, $path)) { $ignore_entry = TRUE; break; } } if ($ignore_entry) { continue; } if (is_dir($path)) { $filemd5s[] = archimedes_directory_hash($path, $ignore); } elseif (is_file($path)) { $filemd5s[] = md5_file($path); } } $d->close(); //sort the md5s before concat so ensure order of files doesn't affect it. asort($filemd5s); return md5(implode('', $filemd5s) . implode('', $symlinks)); }