show = Request::any('show', false, 'string'); if ($this->show === 'panel') { $this->showPanel(); exit(0); } User::load(); if (!User::isLoggedIn()) { Message::addError('main.no-permission'); Util::redirect('?do=Main'); // does not return } $action = Request::post('action'); if ($action === 'writePanelConfig') { $this->writePanelConfig(); $show = 'panels'; } elseif ($action === 'writeLocationConfig') { $this->writeLocationConfig(); $show = 'locations'; } elseif ($action === 'deletePanel') { $this->deletePanel(); $show = 'panels'; } elseif ($action === 'updateServerSettings') { $this->updateServerSettings(); $show = 'backends'; } else { if (($id = Request::post('del-serverid', false, 'int')) !== false) { $this->deleteServer($id); $show = 'backends'; } elseif (($id = Request::post('chk-serverid', false, 'int')) !== false) { $this->checkConnection($id); $show = 'backends'; } elseif (Request::isPost()) { Message::addWarning('main.invalid-action', $action); } } if (Request::isPost() || $this->show === false) { if (!empty($show)) { // } elseif (User::hasPermission('panel.list')) { $show = 'panels'; } elseif (User::hasPermission('location.*')) { $show = 'locations'; } elseif (User::hasPermission('backend.*')) { $show = 'backends'; } else { User::assertPermission('panel.list'); } Util::redirect('?do=locationinfo&show=' . $show); } } /** * Menu etc. has already been generated, now it's time to generate page content. */ protected function doRender() { $data = array('class-' . $this->show => 'active', 'errors' => []); // Do this here so we always see backend errors if (User::hasPermission('backend.*')) { $backends = $this->loadBackends(); foreach ($backends as $backend) { if (!empty($backend['error'])) { $data['errors'][] = $backend; } } } Permission::addGlobalTags($data['perms'], null, ['backend.*', 'location.*', 'panel.list']); Render::addTemplate('page-tabs', $data); switch ($this->show) { case 'locations': $this->showLocationsTable(); break; case 'backends': $this->showBackendsTable($backends ?? []); break; case 'edit-panel': $this->showPanelConfig(); break; case 'panels': $this->showPanelsTable(); break; case 'backendlog': $this->showBackendLog(); break; default: Util::redirect('?do=locationinfo'); } } /** * Deletes the server from the db. */ private function deleteServer($id): void { User::assertPermission('backend.edit'); if ($id === 0) { Message::addError('server-id-missing'); return; } $res = Database::exec("DELETE FROM `locationinfo_coursebackend` WHERE serverid=:id", array('id' => $id)); if ($res !== 1) { Message::addWarning('invalid-server-id', $id); } } private function deletePanel(): void { $id = Request::post('uuid', false, 'string'); if ($id === false) { Message::addError('main.parameter-missing', 'uuid'); return; } $this->assertPanelPermission($id, 'panel.edit'); $res = Database::exec("DELETE FROM `locationinfo_panel` WHERE paneluuid = :id", array('id' => $id)); if ($res !== 1) { Message::addWarning('invalid-panel-id', $id); } if (Module::isAvailable('runmode')) { RunMode::deleteMode(Page::getModule(), $id); } } private static function getTime(string $str): ?int { $str = explode(':', $str); if (count($str) !== 2) return null; if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59) return null; return $str[0] * 60 + $str[1]; } private function writeLocationConfig(): void { // Check locations $locationid = Request::post('locationid', false, 'int'); if ($locationid === false) { Message::addError('main.parameter-missing', 'locationid'); return; } if (Location::get($locationid) === false) { Message::addError('location.invalid-location-id', $locationid); return; } User::assertPermission('location.edit', $locationid); $serverid = Request::post('serverid', 0, 'int'); if ($serverid === 0) { $serverid = null; } $serverlocationid = Request::post('serverlocationid', '', 'string'); $changeServerRecursive = (Request::post('recursive', '', 'string') !== ''); if (empty($serverlocationid) && !$changeServerRecursive) { $insertServerId = null; $ignoreServer = 1; } else { $insertServerId = $serverid; $ignoreServer = 0; } $NOW = time(); Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, serverid, serverlocationid, lastcalendarupdate, lastchange) VALUES (:id, :insertserverid, :serverlocationid, 0, :now) ON DUPLICATE KEY UPDATE serverid = IF(:ignore_server AND serverid IS NULL, NULL, :serverid), serverlocationid = VALUES(serverlocationid), lastcalendarupdate = 0, lastchange = VALUES(lastchange)", array( 'id' => $locationid, 'insertserverid' => $insertServerId, 'serverid' => $serverid, 'serverlocationid' => $serverlocationid, 'ignore_server' => $ignoreServer, 'now' => $NOW, )); if ($changeServerRecursive) { // Recursive overwriting of serverid $array = Location::getAllLocationIds($locationid); if (!empty($array)) { Database::exec("UPDATE locationinfo_locationconfig SET serverid = :serverid, lastcalendarupdate = IF(serverid <> :serverid, 0, lastcalendarupdate), lastchange = :now WHERE locationid IN (:locations)", array( 'serverid' => $serverid, 'locations' => $array, 'now' => $NOW, )); } } } /** * Get all location ids from the locationids parameter, which is comma separated, then split * and remove any ids that don't exist. The cleaned list will be returned. * Will show error and redirect to main page if parameter is missing * * @return int[] list of locations from parameter */ private function getLocationIdsFromRequest(): array { $locationids = Request::post('locationids', Request::REQUIRED_EMPTY); if (is_array($locationids)) { // NOOP } elseif (is_numeric($locationids)) { $locationids = [(int)$locationids]; } elseif (is_string($locationids)) { $locationids = explode(',', $locationids); } $locationids = array_map('intval', $locationids); $all = Location::getAllLocationIds(); $locationids = array_filter($locationids, function ($item) use ($all) { return in_array($item, $all); }); if (empty($locationids)) { Message::addError('main.parameter-empty', 'locationids'); Util::redirect('?do=locationinfo'); } return $locationids; } /** * Updated the config in the db. */ private function writePanelConfig(): void { // UUID - existing or new $paneluuid = Request::post('uuid', false, 'string'); if (($paneluuid === false || strlen($paneluuid) !== 36) && $paneluuid !== 'new') { Message::addError('invalid-panel-id', $paneluuid); Util::redirect('?do=locationinfo', 400); } // Check panel type $paneltype = Request::post('ptype', false, 'string'); switch ($paneltype) { case 'DEFAULT': $params = $this->preparePanelConfigDefault(); break; case 'URL': $params = $this->preparePanelConfigUrl(); break; case 'SUMMARY': case 'UPCOMING': $params = [ 'config' => LocationInfo::panelConfigFromPost($paneltype), 'locationids' => self::getLocationIdsFromRequest(), ]; break; default: Message::addError('invalid-panel-type', $paneltype); Util::redirect('?do=locationinfo', 400); } // Permission $this->assertPanelPermission($paneluuid, 'panel.edit', $params['locationids'] ?? []); if ($paneluuid === 'new') { $paneluuid = Util::randomUuid(); $query = "INSERT INTO `locationinfo_panel` (paneluuid, panelname, locationids, paneltype, panelconfig, lastchange, ispublic) VALUES (:id, :name, :locationids, :type, :config, :now, :ispublic)"; } else { $query = "UPDATE `locationinfo_panel` SET panelname = :name, locationids = :locationids, paneltype = :type, panelconfig = :config, lastchange = :now, ispublic = :ispublic WHERE paneluuid = :id"; } $params['id'] = $paneluuid; $params['name'] = Request::post('panelname', '-', 'string'); $params['type'] = $paneltype; $params['now'] = time(); $params['config'] = json_encode($params['config']); $params['locationids'] = implode(',', $params['locationids']); $params['ispublic'] = Request::post('ispublic', false, 'bool'); Database::exec($query, $params); Message::addSuccess('config-saved'); Util::redirect('?do=locationinfo'); } /** * @return array{config: array, locationids: array} */ private function preparePanelConfigDefault(): array { // Check locations $locationids = self::getLocationIdsFromRequest(); if (count($locationids) > 4) { $locationids = array_slice($locationids, 0, 4); } $conf = LocationInfo::panelConfigFromPost('DEFAULT'); $overrides = []; foreach ($locationids as $lid) { $overrideLoc = Request::post('override'.$lid, false, 'bool'); if ($overrideLoc) { $overrideArray = array( 'mode' => Request::post('override'.$lid.'mode', 1, 'int'), 'roomplanner' => Request::post('override'.$lid.'roomplanner', false, 'bool'), 'vertical' => Request::post('override'.$lid.'vertical', false, 'bool'), 'startday' => Request::post('override'.$lid.'startday', 0, 'int'), 'scaledaysauto' => Request::post('override'.$lid.'scaledaysauto', false, 'bool'), 'daystoshow' => Request::post('override'.$lid.'daystoshow', 7, 'int'), 'rotation' => Request::post('override'.$lid.'rotation', 0, 'int'), 'scale' => Request::post('override'.$lid.'scale', 50, 'int'), 'switchtime' => Request::post('override'.$lid.'switchtime', 60, 'int'), ); $overrides[$lid] = $overrideArray; } } $conf['overrides'] = $overrides; return ['config' => $conf, 'locationids' => $locationids]; } /** * @return array{config: array, locationids: array} */ private function preparePanelConfigUrl(): array { $bookmarkNames = Request::post('bookmarkNames', [], 'array'); $bookmarkUrls = Request::post('bookmarkUrls', [], 'array'); $bookmarkString = ''; for ($i = 0; $i < count($bookmarkNames); $i++) { if (empty($bookmarkNames[$i]) || empty($bookmarkUrls[$i])) continue; $bookmarkString .= rawurlencode($bookmarkNames[$i]) . ',' . rawurlencode($bookmarkUrls[$i]) . ' '; } $bookmarkString = trim($bookmarkString); $conf = LocationInfo::panelConfigFromPost('URL'); $conf['bookmarks'] = $bookmarkString; $conf['accept-language'] = str_replace(['_', ' '], ['-', ''], $conf['accept-language']); return ['config' => $conf, 'locationids' => []]; } /** * Updates the server settings in the db. */ private function updateServerSettings(): void { User::assertPermission('backend.edit'); $serverid = Request::post('id', -1, 'int'); $servername = Request::post('name', 'unnamed', 'string'); $servertype = Request::post('type', '', 'string'); $backend = CourseBackend::getInstance($servertype); if ($backend === false) { Message::addError('invalid-backend-type', $servertype); Util::redirect('?do=locationinfo', 400); } $tmptypeArray = $backend->getCredentialDefinitions(); $credentialsJson = array(); foreach ($tmptypeArray as $cred) { $credentialsJson[$cred->property] = Request::post('prop-' . $cred->property); } $params = array( 'name' => $servername, 'type' => $servertype, 'credentials' => json_encode($credentialsJson) ); if ($serverid === 0) { Database::exec('INSERT INTO `locationinfo_coursebackend` (servername, servertype, credentials) VALUES (:name, :type, :credentials)', $params); $this->checkConnection(Database::lastInsertId()); } else { $params['id'] = $serverid; Database::exec('UPDATE `locationinfo_coursebackend` SET servername = :name, servertype = :type, credentials = :credentials WHERE serverid = :id', $params); $this->checkConnection($serverid); } } /** * Checks if the server connection to a backend is valid. * * @param int $id Server id which connection should be checked. */ private function checkConnection(int $serverid = 0): void { if ($serverid === 0) { ErrorHandler::traceError('checkConnection called with no server id'); } User::assertPermission('backend.check'); $dbresult = Database::queryFirst("SELECT servertype, credentials FROM `locationinfo_coursebackend` WHERE serverid = :serverid", array('serverid' => $serverid)); $serverInstance = CourseBackend::getInstance($dbresult['servertype']); if ($serverInstance === false) { LocationInfo::setServerError($serverid, 'Unknown backend type: ' . $dbresult['servertype']); return; } $credentialsOk = $serverInstance->setCredentials($serverid, (array)json_decode($dbresult['credentials'], true)); if ($credentialsOk) { $serverInstance->checkConnection(); } LocationInfo::setServerError($serverid, $serverInstance->getErrors()); } private function loadBackends(): array { // Get a list of all the backend types. $servertypes = array(); $s_list = CourseBackend::getList(); foreach ($s_list as $s) { $typeInstance = CourseBackend::getInstance($s); $servertypes[$s] = $typeInstance->getDisplayName(); } // Build list of defined backends $serverlist = array(); $dbquery2 = Database::simpleQuery("SELECT * FROM `locationinfo_coursebackend` ORDER BY servername ASC"); foreach ($dbquery2 as $row) { if (isset($servertypes[$row['servertype']])) { $row['typename'] = $servertypes[$row['servertype']]; } else { $row['typename'] = '[' . $row['servertype'] . ']'; $row['disabled'] = 'disabled'; } if (!empty($row['error'])) { $error = json_decode($row['error'], true); if (isset($error['timestamp'])) { $time = date('Y-m-d H:i', $error['timestamp']); } else { $time = '???'; } $row['error'] = $error['error']; $row['errtime'] = $time; } $serverlist[] = $row; } return $serverlist; } /** * Show the list of backends */ private function showBackendsTable(array $serverlist): void { User::assertPermission('backend.*'); $data = array( 'serverlist' => $serverlist, ); Permission::addGlobalTags($data['perms'], null, ['backend.edit', 'backend.check']); // Pass the data to the html and render it. Render::addTemplate('page-servers', $data); } private function showBackendLog(): void { $id = Request::get('serverid', false, 'int'); if ($id === false) { Message::addError('main.parameter-missing', 'serverid'); Util::redirect('?do=locationinfo'); } $server = Database::queryFirst('SELECT servername FROM locationinfo_coursebackend WHERE serverid = :id', ['id' => $id]); if ($server === false) { Message::addError('invalid-server-id', $id); Util::redirect('?do=locationinfo', 400); } $server['list'] = []; $res = Database::simpleQuery('SELECT dateline, message FROM locationinfo_backendlog WHERE serverid = :id ORDER BY logid DESC LIMIT 100', ['id' => $id]); foreach ($res as $row) { $row['dateline_s'] = Util::prettyTime($row['dateline']); $row['class'] = substr($row['message'], 0, 3) === '[F]' ? 'text-danger' : 'text-warning'; $row['message'] = Substr($row['message'], 3); $server['list'][] = $row; } Render::addTemplate('page-server-log', $server); } private function showLocationsTable(): void { $allowedLocations = User::getAllowedLocations('location.edit'); if (empty($allowedLocations)) { Message::addError('main.no-permission'); return; } $locations = Location::getLocations(0, 0, false, true); // Get hidden state of all locations $dbquery = Database::simpleQuery("SELECT li.locationid, li.serverid, li.serverlocationid, li.lastcalendarupdate, loc.openingtime, cb.servertype, cb.servername FROM `locationinfo_locationconfig` AS li LEFT JOIN `locationinfo_coursebackend` AS cb USING (serverid) LEFT JOIN `location` AS loc USING (locationid)"); foreach ($dbquery as $row) { $locid = (int)$row['locationid']; if (!isset($locations[$locid]) || !in_array($locid, $allowedLocations)) continue; $glyph = !empty($row['openingtime']) ? 'ok' : ''; $backend = ''; if (!empty($row['serverid']) && !empty($row['serverlocationid'])) { $backend = $row['servername'] . '(' . $row['serverlocationid'] . ')'; } $locations[$locid] += array( 'openingGlyph' => $glyph, 'strong' => $glyph === 'ok', 'backend' => $backend, 'lastCalendarUpdate' => Util::prettyTime($row['lastcalendarupdate']), // TODO 'backendMissing' => !CourseBackend::exists($row['servertype']), ); } $stack = array(); $depth = -1; foreach ($locations as &$location) { $location['allowed'] = in_array($location['locationid'], $allowedLocations); while ($location['depth'] <= $depth) { array_pop($stack); $depth--; } while ($location['depth'] > $depth) { array_push($stack, empty($location['openingGlyph']) && ($depth === -1 || empty($stack[$depth])) ? '' : 'arrow-up'); $depth++; } if ($depth > 0 && empty($location['openingGlyph'])) { $location['openingGlyph'] = $stack[$depth - 1]; } } Render::addTemplate('page-locations', array( 'list' => array_values($locations), )); } private function showPanelsTable(): void { $visibleLocations = User::getAllowedLocations('panel.list'); if (in_array(0, $visibleLocations)) { $visibleLocations = true; } $editLocations = User::getAllowedLocations('panel.edit'); if (in_array(0, $editLocations)) { $editLocations = true; } $assignLocations = USer::getAllowedLocations('panel.assign-client'); if (in_array(0, $assignLocations)) { $assignLocations = true; } if (empty($visibleLocations)) { Message::addError('main.no-permission'); return; } $res = Database::simpleQuery('SELECT p.paneluuid, p.panelname, p.locationids, p.panelconfig, p.paneltype, p.ispublic FROM locationinfo_panel p ORDER BY panelname ASC'); $hasRunmode = Module::isAvailable('runmode'); if ($hasRunmode) { $runmodes = RunMode::getForModule(Page::getModule(), true); } $panels = array(); foreach ($res as $row) { if ($row['paneltype'] === 'URL') { $url = json_decode($row['panelconfig'], true)['url']; $row['locations'] = $row['locationurl'] = $url; $row['edit_disabled'] = empty($editLocations) ? 'disabled' : ''; $row['runmode_disabled'] = empty($assignLocations) ? 'disabled' : ''; } else { $lids = explode(',', $row['locationids']); // Permissions if ($visibleLocations !== true && !empty(array_diff($lids, $visibleLocations))) { continue; } $row['edit_disabled'] = $editLocations !== true && !empty(array_diff($lids, $editLocations)) ? 'disabled' : ''; $row['runmode_disabled'] = $assignLocations !== true && !empty(array_diff($lids, $assignLocations)) ? 'disabled' : ''; // Locations $locs = array_map(function ($id) { return Location::getName($id) ?: "<>"; }, $lids); $row['locations'] = implode(', ', $locs); } $len = mb_strlen($row['panelname']); if ($len < 3) { $row['panelname'] .= str_repeat(' ', 3 - $len); } if ($hasRunmode && isset($runmodes[$row['paneluuid']])) { $row['assignedMachineCount'] = count($runmodes[$row['paneluuid']]); } $panels[] = $row; } Render::addTemplate('page-panels', compact('panels', 'hasRunmode')); } /** * AJAX */ protected function doAjax() { User::load(); if (!User::isLoggedIn()) { die('Unauthorized'); } $action = Request::any('action'); $id = Request::any('id', 0, 'int'); if ($action === 'config-location') { $this->ajaxConfigLocation($id); } elseif ($action === 'serverSettings') { $this->ajaxServerSettings($id); } } /** * Ajax the server settings. * * @param int $id Serverid */ private function ajaxServerSettings(int $id): void { User::assertPermission('backend.edit'); $oldConfig = Database::queryFirst('SELECT servername, servertype, credentials FROM `locationinfo_coursebackend` WHERE serverid = :id', array('id' => $id)); // Credentials stuff. if ($oldConfig !== false) { $oldCredentials = json_decode($oldConfig['credentials'], true); } else { $oldCredentials = array(); } // Get a list of all the backend types. $serverBackends = array(); $s_list = CourseBackend::getList(); foreach ($s_list as $s) { $backendInstance = CourseBackend::getInstance($s); $backend = array( 'backendtype' => $s, 'display' => $backendInstance->getDisplayName(), 'active' => ($oldConfig !== false && $s === $oldConfig['servertype']), ); $backend['credentials'] = $backendInstance->getCredentialDefinitions(); foreach ($backend['credentials'] as $cred) { /* @var BackendProperty $cred */ if ($backend['active'] && isset($oldCredentials[$cred->property])) { $cred->initForRender($backendInstance->mangleProperty($cred->property, $oldCredentials[$cred->property])); } else { $cred->initForRender(); } $cred->title = Dictionary::translateFile('backend-' . $s, $cred->property); $cred->helptext = Dictionary::translateFile('backend-' . $s, $cred->property . "_helptext"); $cred->credentialsHtml = Render::parse('server-prop-' . $cred->template, (array)$cred); } $serverBackends[] = $backend; } echo Render::parse('ajax-config-server', array('id' => $id, 'name' => $oldConfig['servername'], 'currentbackend' => $oldConfig['servertype'], 'backendList' => $serverBackends, 'defaultBlank' => $oldConfig === false)); } /** * Ajax the time table * * @param int $id id of the location */ private function ajaxConfigLocation(int $id): void { User::assertPermission('location.edit', $id); $locConfig = Database::queryFirst("SELECT info.serverid, info.serverlocationid, loc.openingtime FROM `locationinfo_locationconfig` AS info LEFT JOIN `location` AS loc USING (locationid) WHERE locationid = :id", array('id' => $id)); if ($locConfig !== false) { $openingtimes = json_decode($locConfig['openingtime'], true); } else { $locConfig = array('serverid' => null, 'serverlocationid' => ''); } if (!isset($openingtimes) || !is_array($openingtimes)) { $openingtimes = array(); } // Preset serverid from parent if none is set if (is_null($locConfig['serverid'])) { $chain = Location::getLocationRootChain($id); if (!empty($chain)) { $res = Database::simpleQuery("SELECT serverid, locationid FROM locationinfo_locationconfig WHERE locationid IN (:locations) AND serverid IS NOT NULL", array('locations' => $chain)); $chain = array_flip($chain); $best = false; foreach ($res as $row) { if ($best === false || $chain[$row['locationid']] < $chain[$best['locationid']]) { $best = $row; } } if ($best !== false) { $locConfig['serverid'] = $best['serverid']; } } } // get Server / ID list $res = Database::simpleQuery("SELECT serverid, servername FROM locationinfo_coursebackend ORDER BY servername ASC"); $serverList = array(); foreach ($res as $row) { if ($row['serverid'] == $locConfig['serverid']) { $row['selected'] = 'selected'; } $serverList[] = $row; } $data = array( 'id' => $id, 'serverlist' => $serverList, 'serverlocationid' => $locConfig['serverlocationid'], 'openingtimes' => $this->compressTimes($openingtimes), ); echo Render::parse('ajax-config-location', $data); } /** * Checks if simple mode or expert mode is active. * Tries to merge/compact the opening times schedule, and * will actually modify the passed array iff it can be * transformed into simple opening times. * * @return array new optimized openingtimes */ private function compressTimes(array $array): array { if (empty($array)) return []; // Decompose by day $DAYLIST = array_flip(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']); $new = []; foreach ($array as $row) { $s = Page_LocationInfo::getTime($row['openingtime']); $e = Page_LocationInfo::getTime($row['closingtime']); if ($s === null || $e === null || $e <= $s) continue; foreach ($row['days'] as $day) { $day = $DAYLIST[$day] ?? -1; if ($day === -1) continue; $this->addDay($new, $day, $s, $e); } } // Merge by timespan $merged = []; foreach ($new as $day => $ranges) { foreach ($ranges as $range) { $range = $range[0] . '#' . $range[1]; if (!isset($merged[$range])) { $merged[$range] = []; } $merged[$range][$day] = true; } } // Finally transform to display struct $new = []; foreach ($merged as $span => $days) { $out = explode('#', $span); $new[] = [ 'days' => $this->buildDaysString(array_keys($days)), 'open' => sprintf('%02d:%02d', ($out[0] / 60), ($out[0] % 60)), 'close' => sprintf('%02d:%02d', ($out[1] / 60), ($out[1] % 60)), ]; } return $new; } /** * @param array $daysArray List of days, "Monday", "Tuesday" etc. Must not contain duplicates. * @return string Human-readable representation of list of days */ private function buildDaysString(array $daysArray): string { /* Dictionary::translate('monday') Dictionary::translate('tuesday') Dictionary::translate('wednesday') * Dictionary::translate('thursday') Dictionary::translate('friday') Dictionary::translate('saturday') * Dictionary::translate('sunday') */ $DAYLIST = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; $output = []; $first = $last = -1; sort($daysArray); $daysArray[] = -1; // One trailing element to enforce a flush foreach ($daysArray as $day) { if ($first === -1) { $first = $last = $day; } elseif ($last + 1 === $day) { // Chain $last++; } else { $string = Dictionary::translate($DAYLIST[$first]); if ($first !== $last) { $string .= ($first + 1 === $last ? ",\xe2\x80\x89" : "\xe2\x80\x89-\xe2\x80\x89") . Dictionary::translate($DAYLIST[$last]); } $output[] = $string; $first = $last = $day; } } return implode(', ', $output); } private function addDay(&$array, $day, $s, $e) { if (!isset($array[$day])) { $array[$day] = array(array($s, $e)); return; } foreach (array_keys($array[$day]) as $key) { $current = $array[$day][$key]; if ($s <= $current[0] && $e >= $current[1]) { // Fully dominated unset($array[$day][$key]); continue; // Might partially overlap with additional ranges, keep going } if ($current[0] <= $s && $current[1] >= $s) { // $start lies within existing range if ($current[0] <= $e && $current[1] >= $e) return; // Fully in existing range, do nothing // $end seems to extend range we're checking against but $start lies within this range, update and keep going $s = $current[0]; unset($array[$day][$key]); continue; } // Last possibility: $start is before range, $end within range if ($current[0] <= $e && $current[1] >= $e) { // $start must lie before range start, otherwise we'd have hit the case above $e = $current[1]; unset($array[$day][$key]); //continue; } } $array[$day][] = array($s, $e); } private function showPanelConfig(): void { $id = Request::get('uuid', false, 'string'); if ($id === false) { Message::addError('main.parameter-missing', 'uuid'); return; } $config = false; if (substr($id, 0, 4) === 'new-') { $panel = [ 'paneltype' => substr($id, 4), 'panelname' => '', 'ispublic' => false, ]; $id = 'new'; } else { // Get Config data from db $panel = Database::queryFirst("SELECT panelname, locationids, paneltype, panelconfig, ispublic FROM locationinfo_panel WHERE paneluuid = :id", array('id' => $id)); if ($panel === false) { Message::addError('invalid-panel-id', $id); return; } $config = json_decode($panel['panelconfig'], true); if (!isset($config['roomplanner'])) { $config['roomplanner'] = true; } } // Permission $this->assertPanelPermission($panel, 'panel.edit'); $def = LocationInfo::defaultPanelConfig($panel['paneltype']); if (!is_array($config)) { $config = $def; } else { $config += $def; } $config['panelname'] = $panel['panelname']; $config['ispublic'] = $panel['ispublic']; $configData = LocationInfo::getEditTemplateData($panel['paneltype'], $config); LocationInfo::makeCheckedProperties($panel['paneltype'], $config); if ($panel['paneltype'] === 'DEFAULT') { // The override part is unfortunately too specialized to use the LocationInfo::getEditTemplateData() // helper, so we keep using our specialized template Render::addTemplate('page-config-panel-default', [ 'new' => $id === 'new', 'uuid' => $id, 'panelname' => $panel['panelname'] ?? '', 'locations' => Location::getLocations(), 'locationids' => $panel['locationids'] ?? '', 'overrides' => json_encode($config['overrides']), 'ispublic_checked' => $config['ispublic'] ? 'checked' : '', ] + $config); } elseif ($panel['paneltype'] === 'URL') { // Bookmarks are handled differently $bookmarksArray = []; if (!empty($config['bookmarks'])) { $bookmarksConfig = explode(' ', $config['bookmarks']); foreach ($bookmarksConfig AS $bookmark) { $bookmark = explode(',', $bookmark); $name = rawurldecode($bookmark[0]); $url = rawurldecode($bookmark[1]); $bookmarksArray[] = [ 'name' => $name, 'url' => $url, ]; } } unset($config['bookmarks']); // URL filtering also has a custom style that can't be handled generically LocationInfo::cleanupUrlFilter($config); Render::addTemplate('page-config-panel-url', array( 'new' => $id === 'new', 'uuid' => $id, 'sections' => $configData, 'bookmarks' => $bookmarksArray, 'whitelist' => $config['whitelist'], 'blacklist' => $config['blacklist'], )); } else { // Use generic edit template Render::addTemplate('page-config-panel-generic', array( 'new' => $id === 'new', 'uuid' => $id, 'sections' => $configData, 'paneltype' => $panel['paneltype'], 'heading-hint' => Dictionary::translateFile('panel-params', 'intro-' . $panel['paneltype']), 'locations' => Location::getLocations(explode(',', $panel['locationids'] ?? '')), )); } } private function showPanel(): void { $uuid = Request::get('uuid', false, 'string'); if ($uuid === false) { http_response_code(400); die('Missing parameter uuid'); } $config = []; $type = InfoPanel::getConfig($uuid, $config); if ($type === null) { http_response_code(404); die('Panel with given uuid not found'); } if ($type === 'URL') { // Shortcut - URL panel is just a redirect Util::redirect($config['url']); } // Figure out if the panel is being accessed directly, or via the pretty rewrite preg_match('#^/(.*)/#', $_SERVER['PHP_SELF'], $script); preg_match('#^/([^?]+)/#', $_SERVER['REQUEST_URI'], $request); if ($script[1] !== $request[1]) { // Working with server-side rewrites $config['api'] = 'api/'; } else { // 1:1 $config['api'] = 'api.php?do=locationinfo&'; } // Check if we have a customized template, build filename $templateFile = 'frontend-' . strtolower($type); if (!empty($config['mod']) && preg_match('#^[a-z0-9]+$#', $config['mod']) && file_exists('modules/locationinfo/templates/' . $templateFile . '-' . $config['mod'] . '.html')) { $templateFile .= '-' . $config['mod']; } $lang = $config['language'] ?? 'en'; $reqLang = Request::get('forcelang', false, 'string'); if ($reqLang !== false && Dictionary::hasLanguage($reqLang)) { // Language overridden by parameter? $lang = $reqLang; } elseif ($config['language-system'] ?? false) { // Language overridden by client's browser headers? $langs = preg_split('/[,\s]+/', $_SERVER['HTTP_ACCEPT_LANGUAGE']); foreach ($langs as $check) { $check = substr($check, 0, 2); if (Dictionary::hasLanguage($check)) { $lang = $check; break; } } } unset($config['language'], $config['language-system']); $config['uuid'] = $uuid; $config['language'] = $lang; if ($type === 'SUMMARY') { $locations = LocationInfo::getLocationsOr404($uuid, false); $config['tree'] = Location::getTree(...$locations); } elseif ($type === 'UPCOMING') { $bg = $this->parseCssColor($config['color_bg'] ?? '#000'); if ($bg !== null) { $config['color_grad1'] = 'rgba(' . $bg[0] . ',' . $bg[1] . ',' . $bg[2] . ', 0)'; $config['color_grad2'] = 'rgba(' . $bg[0] . ',' . $bg[1] . ',' . $bg[2] . ', .1)'; $config['color_grad3'] = 'rgba(' . $bg[0] . ',' . $bg[1] . ',' . $bg[2] . ', .9)'; } $fg = $this->parseCssColor($config['color_fg'] ?? '#fff'); if ($fg !== null) { $config['color_half'] = 'rgba(' . $fg[0] . ',' . $fg[1] . ',' . $fg[2] . ', .5)'; } } $config['config'] = json_encode($config); // Must come after all config options needed in JSON are set $config['jsbump'] = 6; // Bump this every time you touch a JS file do ensure it gets reloaded die(Render::parse($templateFile, $config, null, $lang)); } /** * @param string|array $panelOrUuid UUID of panel, or array with keys paneltype and locationds * @param int[] $additionalLocations */ private function assertPanelPermission($panelOrUuid, string $permission, ?array $additionalLocations = null): void { if (is_array($panelOrUuid)) { $panel = $panelOrUuid; } else { $panel = Database::queryFirst('SELECT paneltype, locationids FROM locationinfo_panel WHERE paneluuid = :uuid', ['uuid' => $panelOrUuid]); } if ($panel === false || $panel['paneltype'] === 'URL' || empty($panel['locationids'])) { if (empty($additionalLocations)) { User::assertPermission($permission, null, '?do=locationinfo'); return; } } $allowed = User::getAllowedLocations($permission); if (in_array(0, $allowed)) return; if (!empty($allowed)) { if (isset($panel['locationids'])) { $locations = explode(',', $panel['locationids']); } else { $locations = []; } if (!empty($additionalLocations)) { $locations = array_merge($locations, $additionalLocations); } if (empty(array_diff($locations, $allowed))) return; } Message::addError('main.no-permission'); Util::redirect('?do=locationinfo'); } /** * Parse a CSS color parameter into an array of color components. * @param string $param The CSS color parameter to parse. * @return ?array An array containing the color components [R, G, B, A] if valid, or null if parsing fails. */ private function parseCssColor(string $param): ?array { $param = trim($param); // #fff if (preg_match('/^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/', $param, $out)) return [hexdec($out[1]) * 17, hexdec($out[2]) * 17, hexdec($out[3]) * 17, 1]; // #ffffff if (preg_match('/^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/', $param, $out)) return [hexdec($out[1]), hexdec($out[2]), hexdec($out[3]), 1]; // rgb(255,255,255) if (preg_match('/^rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)$/i', $param, $out)) return [(int)($out[1]), (int)($out[2]), (int)($out[3]), 1]; // rgba(255,255,255,1) if (preg_match('/^rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9.]{1,5})\s*\)$/i', $param, $out)) return [(int)$out[1], (int)$out[2], (int)$out[3], (float)$out[4]]; return null; } }