diff options
Diffstat (limited to 'modules-available/locationinfo/inc/coursebackend.inc.php')
-rw-r--r-- | modules-available/locationinfo/inc/coursebackend.inc.php | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/modules-available/locationinfo/inc/coursebackend.inc.php b/modules-available/locationinfo/inc/coursebackend.inc.php new file mode 100644 index 00000000..1fe87202 --- /dev/null +++ b/modules-available/locationinfo/inc/coursebackend.inc.php @@ -0,0 +1,365 @@ +<?php + +/** + * Base class for course query backends + */ +abstract class CourseBackend +{ + + /* + * Static part for handling interfaces + */ + + /** + * @var array list of known backends + */ + private static $backendTypes = false; + /** + * @var boolean|string false = no error, error message otherwise + */ + protected $error; + /** + * @var int as internal serverId + */ + protected $serverId; + /** + * @const int max number of additional locations to fetch (for backends that benefit from request coalesc.) + */ + const MAX_ADDIDIONAL_LOCATIONS = 5; + + /** + * CourseBackend constructor. + */ + public final function __construct() + { + $this->error = false; + } + + /** + * Load all known backend types. This is done + * by including *.inc.php from inc/coursebackend/. + */ + public static function loadDb() + { + if (self::$backendTypes !== false) + return; + self::$backendTypes = array(); + foreach (glob(dirname(__FILE__) . '/coursebackend/coursebackend_*.inc.php', GLOB_NOSORT) as $file) { + require_once $file; + preg_match('#coursebackend_([^/\.]+)\.inc\.php$#i', $file, $out); + if (!class_exists('coursebackend_' . $out[1])) { + trigger_error("Backend type source unit $file doesn't seem to define class CourseBackend_{$out[1]}", E_USER_ERROR); + } + self::$backendTypes[$out[1]] = true; + } + } + + /** + * Get all known config module types. + * + * @return array list of modules + */ + public static function getList() + { + self::loadDb(); + return array_keys(self::$backendTypes); + } + + /** + * Get fresh instance of ConfigModule subclass for given module type. + * + * @param string $moduleType name of module type + * @return \CourseBackend module instance + */ + public static function getInstance($moduleType) + { + self::loadDb(); + if (!isset(self::$backendTypes[$moduleType])) { + error_log('Unknown module type: ' . $moduleType); + return false; + } + if (!is_object(self::$backendTypes[$moduleType])) { + $class = "coursebackend_$moduleType"; + self::$backendTypes[$moduleType] = new $class; + } + return self::$backendTypes[$moduleType]; + } + + /** + * @return string return display name of backend + */ + public abstract function getDisplayName(); + + + /** + * @returns \BackendProperty[] list of properties that need to be set + */ + public abstract function getCredentialDefinitions(); + + /** + * @return boolean true if the connection works, false otherwise + */ + public abstract function checkConnection(); + + /** + * uses json to setCredentials, the json must follow the form given in + * getCredentials + * + * @param array $data assoc array with data required by backend + * @returns bool if the credentials were in the correct format + */ + public abstract function setCredentialsInternal($data); + + /** + * @return int desired caching time of results, in seconds. 0 = no caching + */ + public abstract function getCacheTime(); + + /** + * @return int age after which timetables are no longer refreshed should be + * greater then CacheTime + */ + public abstract function getRefreshTime(); + + /** + * Internal version of fetch, to be overridden by subclasses. + * + * @param $roomIds array with local ID as key and serverId as value + * @return array a recursive array that uses the roomID as key + * and has the schedule array as value. A shedule array contains an array in this format: + * ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string] + */ + protected abstract function fetchSchedulesInternal($roomId); + + /** + * Method for fetching the schedule of the given rooms on a server. + * + * @param array $roomId array of room ID to fetch + * @return array|bool array containing the timetables as value and roomid as key as result, or false on error + */ + public final function fetchSchedule($requestedLocationIds) + { + if (!is_array($requestedLocationIds)) { + $this->error = 'No array of roomids was given to fetchSchedule'; + return false; + } + if (empty($requestedLocationIds)) + return array(); + $NOW = time(); + $dbquery1 = Database::simpleQuery("SELECT locationid, calendar, serverlocationid, lastcalendarupdate + FROM locationinfo_locationconfig WHERE locationid IN (:locations)", + array('locations' => array_values($requestedLocationIds))); + $returnValue = []; + $remoteIds = []; + while ($row = $dbquery1->fetch(PDO::FETCH_ASSOC)) { + //Check if in cache if lastUpdate is null then it is interpreted as 1970 + if ($row['lastcalendarupdate'] + $this->getCacheTime() > $NOW) { + $returnValue[$row['locationid']] = json_decode($row['calendar']); + } else { + $remoteIds[$row['locationid']] = $row['serverlocationid']; + } + + } + // No need for additional round trips to backend + if (empty($remoteIds)) { + return $returnValue; + } + // Check if we should refresh other rooms recently requested by front ends + if ($this->getRefreshTime() > $this->getCacheTime()) { + $dbquery4 = Database::simpleQuery("SELECT locationid, serverlocationid FROM locationinfo_locationconfig + WHERE serverid = :serverid AND serverlocationid NOT IN (:skiplist) + AND lastcalendarupdate BETWEEN :lowerage AND :upperage + LIMIT " . self::MAX_ADDIDIONAL_LOCATIONS, array( + 'serverid' => $this->serverId, + 'skiplist' => array_values($remoteIds), + 'lowerage' => $NOW - $this->getRefreshTime(), + 'upperage' => $NOW - $this->getCacheTime(), + )); + while ($row = $dbquery4->fetch(PDO::FETCH_ASSOC)) { + $remoteIds[$row['locationid']] = $row['serverlocationid']; + } + } + $backendResponse = $this->fetchSchedulesInternal($remoteIds); + if ($backendResponse === false) { + return false; + } + + if ($this->getCacheTime() > 0) { + // Caching requested by backend, write to DB + foreach ($backendResponse as $serverRoomId => $calendar) { + $value = json_encode($calendar); + Database::simpleQuery("UPDATE locationinfo_locationconfig SET calendar = :ttable, lastcalendarupdate = :now + WHERE serverid = :serverid AND serverlocationid = :serverlocationid", array( + 'serverid' => $this->serverId, + 'serverlocationid' => $serverRoomId, + 'ttable' => $value, + 'now' => $NOW + )); + } + } + // Add rooms that were requested to the final return value + foreach ($remoteIds as $location => $serverRoomId) { + if (isset($backendResponse[$serverRoomId]) && in_array($location, $requestedLocationIds)) { + // Only add if we can map it back to our location id AND it was not an unsolicited coalesced refresh + $returnValue[$location] = $backendResponse[$serverRoomId]; + } + } + + return $returnValue; + } + + public final function setCredentials($serverId, $data) + { + foreach ($this->getCredentialDefinitions() as $prop) { + if (!isset($data[$prop->property])) { + $data[$prop->property] = $prop->default; + } + if (in_array($prop->type, ['string', 'bool', 'int'])) { + settype($data[$prop->property], $prop->type); + } else { + settype($data[$prop->property], 'string'); + } + } + if ($this->setCredentialsInternal($data)) { + $this->serverId = $serverId; + return true; + } + return false; + } + + /** + * @return false if there was no error string with error message if there was one + */ + public final function getError() + { + return $this->error; + } + + /** + * Query path in array-representation of XML document. + * e.g. 'path/syntax/foo/wanteditem' + * This works for intermediate nodes (that have more children) + * and leaf nodes. The result is always an array on success, or + * false if not found. + */ + protected function getArrayPath($array, $path) + { + if (!is_array($path)) { + // Convert 'path/syntax/foo/wanteditem' to array for further processing and recursive calls + $path = explode('/', $path); + } + if (isset($array[0])) { + // The currently handled element of the path exists multiple times on the current level, so it is + // wrapped in a plain array - recurse into each one of them and merge the results + $return = []; + foreach ($array as $item) { + $test = $this->getArrayPath($item, $path); + If (is_array($test)) { + $return = array_merge($return, $test); + } + + } + return $return; + } + do { + // Get next element from array, loop to ignore empty elements (so double slashes in the path are allowed) + $element = array_shift($path); + } while (empty($element) && !empty($path)); + if (!isset($array[$element])) { + // Current path element does not exist - error + return false; + } + if (empty($path)) { + // Path is now empty which means we're at 'wanteditem' from out example above + if (!is_array($array[$element]) || !isset($array[$element][0])) { + // If it's a leaf node of the array, wrap it in plain array, so the function will + // always return an array on success + return array($array[$element]); + } + // 'wanteditem' is not a unique leaf node, return as is + // This means it's either a plain array, in case there are multiple 'wanteditem' elements on the same level + // or it's an associative array if 'wanteditem' has any sub-nodes + return $array[$element]; + } + // Recurse + if (!is_array($array[$element])) { + // We're in the middle of the requested path, but the current element is already a leaf node with no + // children - error + return false; + } + // Non-leaf node - simple recursion + return $this->getArrayPath($array[$element], $path); + } + + /** + * @param string $response xml document to convert + * @return bool|array array representation of the xml if possible, false otherwise + */ + protected function xmlStringToArray($response) + { + $cleanresponse = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $response); + try { + $xml = new SimpleXMLElement($cleanresponse); + } catch (Exception $e) { + $this->error = 'Could not parse reply as XML, got ' . get_class($e) . ': ' . $e->getMessage(); + return false; + } + $array = json_decode(json_encode((array)$xml), true); + return $array; + } + +} + +/** + * Class BackendProperty describes a property a backend requires to define its functionality + */ +class BackendProperty { + public $property; + public $type; + public $default; + public function __construct($property, $type, $default = '') + { + $this->property = $property; + $this->type = $type; + $this->default = $default; + } + + /** + * Initialize additional fields of this class that are only required + * for rendering the server configuration dialog. + * + * @param string $backendId target backend id + * @param mixed $current current value of this property. + */ + public function initForRender($current = null) { + if (is_array($this->type)) { + $this->template = 'dropdown'; + $this->select_list = []; + foreach ($this->type as $item) { + $this->select_list[] = [ + 'option' => $item, + 'active' => $item == $current, + ]; + } + } elseif ($this->type === 'bool') { + $this->template = $this->type; + } else { + $this->template = 'generic'; + } + if ($this->type === 'string') { + $this->inputtype = 'text'; + } elseif ($this->type === 'int') { + $this->inputtype = 'number'; + } elseif ($this->type === 'password') { + $this->inputtype = Property::getPasswordFieldType(); + } + $this->currentvalue = $current === null ? $this->default : $current; + } + public $inputtype; + public $template; + public $title; + public $helptext; + public $currentvalue; + public $select_list; + public $credentialsHtml; +} |