diff options
Diffstat (limited to 'modules-available/locations/pages')
-rw-r--r-- | modules-available/locations/pages/cleanup.inc.php | 9 | ||||
-rw-r--r-- | modules-available/locations/pages/details.inc.php | 259 | ||||
-rw-r--r-- | modules-available/locations/pages/locations.inc.php | 183 | ||||
-rw-r--r-- | modules-available/locations/pages/subnets.inc.php | 10 |
4 files changed, 301 insertions, 160 deletions
diff --git a/modules-available/locations/pages/cleanup.inc.php b/modules-available/locations/pages/cleanup.inc.php index d10dbac0..423d6a6b 100644 --- a/modules-available/locations/pages/cleanup.inc.php +++ b/modules-available/locations/pages/cleanup.inc.php @@ -3,7 +3,7 @@ class SubPage { - public static function doPreprocess($action) + public static function doPreprocess($action): bool { if ($action === 'resetmachines') { self::resetMachines(); @@ -16,17 +16,20 @@ class SubPage return false; } - public static function doRender($action) + public static function doRender($action): bool { $list = self::loadForLocation(); if ($list === false) return true; + $list['canmove'] = array_reduce($list['clients'], function (bool $carry, array $item): bool { + return $carry || ($item['canmove'] ?? false); + }, false); Permission::addGlobalTags($list['perms'], NULL, ['subnets.edit', 'location.view']); Render::addTemplate('mismatch-cleanup', $list); return true; } - public static function doAjax($action) + public static function doAjax($action): bool { return false; } diff --git a/modules-available/locations/pages/details.inc.php b/modules-available/locations/pages/details.inc.php index 81b58456..279eee44 100644 --- a/modules-available/locations/pages/details.inc.php +++ b/modules-available/locations/pages/details.inc.php @@ -3,29 +3,121 @@ class SubPage { - public static function doPreprocess($action) + public static function doPreprocess($action): bool { if ($action === 'updatelocation') { self::updateLocation(); return true; } + if ($action === 'updateOpeningtimes') { + self::updateOpeningTimes(); + return true; + } return false; } - public static function doRender($action) + public static function doRender($action): bool { return false; } - public static function doAjax($action) + public static function doAjax($action): bool { if ($action === 'showlocation') { self::ajaxShowLocation(); return true; + } elseif ($action === 'getOpeningtimes') { + $id = Request::any('locid', 0, 'int'); + self::ajaxOpeningTimes($id); + return true; } return false; } + private static function updateOpeningTimes() + { + $otInherited = Request::post('openingtimes-inherited', false, 'bool'); + $openingTimes = Request::post('openingtimes', Request::REQUIRED, 'string'); + $locationid = Request::post('locationid', Request::REQUIRED, 'int'); + $wol = Request::post('wol', false, 'bool'); + $wolOffset = Request::post('wol-offset', 0, 'int'); + $sd = Request::post('sd', false, 'bool'); + $sdOffset = Request::post('sd-offset', 0, 'int'); + $raMode = Request::post('ra-mode', 'ALWAYS', 'string'); + + User::assertPermission('location.edit.openingtimes', $locationid); + + // Construct opening-times for database + if ($otInherited || $openingTimes === '') { + $openingTimes = null; + } else { + $openingTimes = json_decode($openingTimes, true); + if (!is_array($openingTimes)) { + $openingTimes = null; + } else { + $mangled = array(); + foreach (array_keys($openingTimes) as $key) { + $entry = $openingTimes[$key]; + if (empty($entry['days']) || !is_array($entry['days'])) { + Message::addError('ignored-line-no-days'); + continue; + } + $start = self::getTime($entry['openingtime']); + $end = self::getTime($entry['closingtime']); + if ($start === false) { + Message::addError('ignored-invalid-start', $entry['openingtime']); + continue; + } + if ($end === false) { + Message::addError('ignored-invalid-end', $entry['closingtime']); + continue; + } + if ($end <= $start) { + Message::addError('ignored-invalid-range', $entry['openingtime'], $entry['closingtime']); + continue; + } + unset($entry['tag']); + $mangled[] = $entry; + } + if (empty($mangled)) { + $openingTimes = null; + } else { + $openingTimes = json_encode($mangled); + } + } + } + // Check if opening-times changed + // $res = Database::queryFirst('SELECT openingtime FROM location WHERE locationid = :locationid', compact('locationid')); + // $otChanged = $res === false || $res['openingtime'] !== $openingTimes; + + Database::exec('UPDATE location SET openingtime = :openingtime WHERE locationid = :locationid', + array('locationid' => $locationid, 'openingtime' => $openingTimes)); + + if (Module::isAvailable('rebootcontrol')) { + // Set options + if (!Scheduler::isValidRaMode($raMode)) { + $raMode = Scheduler::RA_ALWAYS; + } + Scheduler::setLocationOptions($locationid, [ + 'wol' => $wol, + 'sd' => $sd, + 'wol-offset' => $wolOffset, + 'sd-offset' => $sdOffset, + 'ra-mode' => $raMode, + ]); + } + } + + private static function getTime($str) + { + $str = explode(':', $str); + if (count($str) !== 2) + return false; + if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59) + return false; + return $str[0] * 60 + $str[1]; + } + private static function updateLocation() { $locationId = Request::post('locationid', false, 'integer'); @@ -81,7 +173,7 @@ class SubPage Util::redirect('?do=Locations'); } - private static function updateLocationData($location) + private static function updateLocationData(array $location): bool { $locationId = (int)$location['locationid']; $newParent = Request::post('parentlocationid', false, 'integer'); @@ -125,14 +217,12 @@ class SubPage return $newParent != $location['parentlocationid']; } - private static function updateLocationSubnets() + private static function updateLocationSubnets(): bool { $locationId = Request::post('locationid', false, 'integer'); if (!User::hasPermission('location.edit.subnets', $locationId)) return false; - $change = false; - // Deletion first $dels = Request::post('deletesubnet', false); $deleteCount = 0; @@ -151,8 +241,9 @@ class SubPage $starts = Request::post('startaddr', false); $ends = Request::post('endaddr', false); if (!is_array($starts) || !is_array($ends)) { - return $change; + return false; } + $change = false; $editCount = 0; $stmt = Database::prepare('UPDATE subnet SET startaddr = :start, endaddr = :end' . ' WHERE subnetid = :id'); @@ -167,7 +258,7 @@ class SubPage continue; } $range = LocationUtil::rangeToLongVerbose($start, $end); - if ($range === false) + if ($range === null) continue; list($startLong, $endLong) = $range; if ($stmt->execute(array('id' => $subnetid, 'start' => $startLong, 'end' => $endLong))) { @@ -185,18 +276,18 @@ class SubPage return $change; } - private static function addNewLocationSubnets($location) + private static function addNewLocationSubnets(array $location): bool { $locationId = (int)$location['locationid']; if (!User::hasPermission('location.edit.subnets', $locationId)) return false; - $change = false; $starts = Request::post('newstartaddr', false); $ends = Request::post('newendaddr', false); if (!is_array($starts) || !is_array($ends)) { - return $change; + return false; } + $change = false; $count = 0; $stmt = Database::prepare('INSERT INTO subnet SET startaddr = :start, endaddr = :end, locationid = :location'); foreach ($starts as $key => $start) { @@ -241,7 +332,7 @@ class SubPage $res = Database::simpleQuery("SELECT subnetid, startaddr, endaddr FROM subnet WHERE locationid = :lid", array('lid' => $locationId)); $rows = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $row['startaddr'] = long2ip($row['startaddr']); $row['endaddr'] = long2ip($row['endaddr']); $rows[] = $row; @@ -251,6 +342,7 @@ class SubPage 'locationname' => $loc['locationname'], 'list' => $rows, 'roomplanner' => Module::get('roomplanner') !== false, + 'news' => Module::get('news') !== false && User::hasPermission('.news.*', $loc['locationid']), 'parents' => Location::getLocations($loc['parentlocationid'], $locationId, true) ); @@ -279,7 +371,7 @@ class SubPage if (Module::get('statistics') !== false) { $mres = Database::simpleQuery("SELECT state FROM machine" . " WHERE machine.locationid = :lid", array('lid' => $locationId)); - while ($row = $mres->fetch(PDO::FETCH_ASSOC)) { + foreach ($mres as $row) { $count++; if ($row['state'] === 'IDLE') { $online++; @@ -303,14 +395,151 @@ class SubPage $data['used_percent'] = $count === 0 ? 0 : round(($used / $count) * 100); - Permission::addGlobalTags($data['perms'], $locationId, ['location.edit.name', 'location.edit.subnets', 'location.delete', '.roomplanner.edit'], 'save_button'); + Permission::addGlobalTags($data['perms'], $locationId, + ['location.edit.name', 'location.edit.subnets', 'location.delete', 'location.edit.openingtimes', '.roomplanner.edit'], + 'save_button'); if (empty($allowedLocs)) { $data['perms']['location']['edit']['parent']['disabled'] = 'disabled'; } else { unset($data['perms']['save_button']); } + if (Module::get('rebootcontrol') !== false) { + $res = Database::queryFirst("SELECT action, nextexecution FROM `reboot_scheduler` + WHERE locationid = :id", ['id' => $locationId]); + if ($res !== false && $res['nextexecution'] > 0) { + $data['next_action'] = $res['action']; + $data['next_time'] = Util::prettyTime($res['nextexecution']); + } + } + echo Render::parse('location-subnets', $data); } + private static function ajaxOpeningTimes($id) + { + User::assertPermission('location.edit.openingtimes', $id); + $data = ['id' => $id]; + $openTimes = Database::queryFirst("SELECT openingtime FROM `location` + WHERE locationid = :id", array('id' => $id)); + if ($openTimes === false) { + Message::addError('invalid-location-id', $id); + return; + } + if ($openTimes['openingtime'] !== null) { + $openingTimes = json_decode($openTimes['openingtime'], true); + } else { + $openingTimes = OpeningTimes::forLocation($id); + $data['openingtimes_inherited'] = 'checked'; + } + if (!isset($openingTimes) || !is_array($openingTimes)) { + $openingTimes = array(); + } + $data['expertMode'] = !self::isSimpleMode($openingTimes); + $data['schedule_data'] = json_encode($openingTimes); + + $rebootcontrol = Module::isAvailable('rebootcontrol'); + $data['rebootcontrol'] = $rebootcontrol; + if ($rebootcontrol) { + $data['scheduler-options'] = Scheduler::getLocationOptions($id); + $data['scheduler_' . $data['scheduler-options']['ra-mode'] . '_checked'] = 'checked'; + } + + echo Render::parse('ajax-opening-location', $data); + } + + private static function isSimpleMode(&$array): bool + { + if (empty($array)) + return true; + // Decompose by day + $new = array(); + foreach ($array as $row) { + $s = self::getTime($row['openingtime']); + $e = self::getTime($row['closingtime']); + if ($s === false || $e === false || $e <= $s) + continue; + foreach ($row['days'] as $day) { + self::addDay($new, $day, $s, $e); + } + } + // Merge by timespan, but always keep saturday and sunday separate + $merged = array(); + foreach ($new as $day => $ranges) { + foreach ($ranges as $range) { + if ($day === 'Saturday' || $day === 'Sunday') { + $add = $day; + } else { + $add = ''; + } + $key = '#' . $range[0] . '#' . $range[1] . '#' . $add; + if (!isset($merged[$key])) { + $merged[$key] = array(); + } + $merged[$key][$day] = true; + } + } + // Check if it passes as simple mode + if (count($merged) > 3) + return false; + foreach ($merged as $days) { + if (count($days) === 5) { + $res = array_keys($days); + $res = array_intersect($res, array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")); + if (count($res) !== 5) + return false; + } elseif (count($days) === 1) { + if (!isset($days['Saturday']) && !isset($days['Sunday'])) { + return false; + } + } else { + return false; + } + } + // Valid simple mode, finally transform back to what we know + $new = array(); + foreach ($merged as $span => $days) { + preg_match('/^#(\d+)#(\d+)#/', $span, $out); + $new[] = array( + 'days' => array_keys($days), + 'openingtime' => floor($out[1] / 60) . ':' . ($out[1] % 60), + 'closingtime' => floor($out[2] / 60) . ':' . ($out[2] % 60), + ); + } + $array = $new; + return true; + } + + private static 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); + } }
\ No newline at end of file diff --git a/modules-available/locations/pages/locations.inc.php b/modules-available/locations/pages/locations.inc.php index 8afb454a..78818328 100644 --- a/modules-available/locations/pages/locations.inc.php +++ b/modules-available/locations/pages/locations.inc.php @@ -3,7 +3,7 @@ class SubPage { - public static function doPreprocess($action) + public static function doPreprocess($action): bool { if ($action === 'addlocations') { self::addLocations(); @@ -12,7 +12,7 @@ class SubPage return false; } - public static function doRender($getAction) + public static function doRender($getAction): bool { if ($getAction === false) { if (User::hasPermission('location.view')) { @@ -32,7 +32,7 @@ class SubPage return false; } - public static function doAjax($action) + public static function doAjax($action): bool { return false; } @@ -93,126 +93,43 @@ class SubPage $unassignedIdle = $unassignedLoad = $unassignedOverrides = 0; $allowedLocationIds = User::getAllowedLocations("location.view"); - foreach (array_keys($locationList) as $lid) { - if (!User::hasPermission('.baseconfig.view', $lid)) { - $locationList[$lid]['havebaseconfig'] = false; - } - if (!User::hasPermission('.sysconfig.config.view-list', $lid)) { - $locationList[$lid]['havesysconfig'] = false; - } - if (!User::hasPermission('.statistics.view.list', $lid)) { - $locationList[$lid]['havestatistics'] = false; - } - if (!User::hasPermission('.serversetup.ipxe.menu.assign', $lid)) { - $locationList[$lid]['haveipxe'] = false; - } - if (!in_array($lid, $allowedLocationIds)) { - $locationList[$lid]['show-only'] = true; - } - } - // Client statistics - if (Module::get('statistics') !== false) { - $unassigned = 0; - $extra = ''; - if (in_array(0, $allowedLocationIds)) { - $extra = ' OR locationid IS NULL'; - } - $res = Database::simpleQuery("SELECT m.locationid, Count(*) AS cnt, - Sum(If(m.state = 'OCCUPIED', 1, 0)) AS used, Sum(If(m.state = 'IDLE', 1, 0)) AS idle - FROM machine m WHERE (locationid IN (:allowedLocationIds) $extra) GROUP BY locationid", compact('allowedLocationIds')); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $locId = (int)$row['locationid']; - if (isset($locationList[$locId])) { - $locationList[$locId]['clientCount'] = $row['cnt']; - $locationList[$locId]['clientLoad'] = round(100 * $row['used'] / $row['cnt']); - $locationList[$locId]['clientIdle'] = round(100 * ($row['used'] + $row['idle']) / $row['cnt']); - } else { - $unassigned += $row['cnt']; - $unassignedLoad += $row['used']; - $unassignedIdle += $row['idle']; - } - } - $res = Database::simpleQuery("SELECT m.locationid, Count(DISTINCT sm.machineuuid) AS cnt FROM setting_machine sm - INNER JOIN machine m USING (machineuuid) GROUP BY m.locationid"); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $locId = (int)$row['locationid']; - if (isset($locationList[$locId])) { - $locationList[$locId]['machineVarsOverrideCount'] = $row['cnt']; - } else { - $unassignedOverrides += $row['cnt']; - } - } - unset($loc); - foreach ($locationList as &$loc) { - if (!in_array($loc['locationid'], $allowedLocationIds)) - continue; - if (!isset($loc['clientCountSum'])) { - $loc['clientCountSum'] = 0; - } - if (!isset($loc['clientCount'])) { - $loc['clientCount'] = $loc['clientIdle'] = $loc['clientLoad'] = 0; - } - $loc['clientCountSum'] += $loc['clientCount']; - foreach ($loc['parents'] as $pid) { - if (!in_array($pid, $allowedLocationIds)) - continue; - $locationList[(int)$pid]['hasChild'] = true; - $locationList[(int)$pid]['clientCountSum'] += $loc['clientCount']; - } - } - unset($loc); - - } - // Show currently active sysconfig for each location - $defaultConfig = false; - if (Module::isAvailable('sysconfig')) { - $confs = SysConfig::getAll(); - foreach ($confs as $conf) { - if (strlen($conf['locs']) === 0) - continue; - $confLocs = explode(',', $conf['locs']); - foreach ($confLocs as $locId) { - settype($locId, 'int'); - if ($locId === 0) { - $defaultConfig = $conf['title']; + $plugins = []; + foreach (Hook::load('locations-column') as $hook) { + $c = @include($hook->file); + if ($c instanceof AbstractLocationColumn) { + $plugins[sprintf('%04d.%s', $c->priority(), $hook->moduleId)] = $c; + } elseif (is_array($c)) { + foreach ($c as $i => $cc) { + if ($cc instanceof AbstractLocationColumn) { + $plugins[sprintf('%04d.%d.%s', $cc->priority(), $i, $hook->moduleId)] = $cc; } - if (!isset($locationList[$locId])) - continue; - $locationList[$locId] += array('configName' => $conf['title'], 'configClass' => 'slx-bold'); } } - self::propagateFields($locationList, $defaultConfig, 'configName', 'configClass'); } - // Count overridden config vars - if (Module::get('baseconfig') !== false) { - $res = Database::simpleQuery("SELECT locationid, Count(*) AS cnt FROM `setting_location` - WHERE locationid IN (:allowedLocationIds) GROUP BY locationid", compact('allowedLocationIds')); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $lid = (int)$row['locationid']; - if (isset($locationList[$lid])) { - $locationList[$lid]['overriddenVars'] = $row['cnt']; - } + ksort($plugins); + foreach ($locationList as $lid => &$loc) { + $loc['plugins'] = []; + foreach ($plugins as $pk => $plugin) { + $loc['plugins'][$pk] = [ + 'url' => $plugin->getEditUrl($lid), + 'html' => $plugin->getColumnHtml($lid), + ]; + } + if (!in_array($lid, $allowedLocationIds)) { + $locationList[$lid]['show-only'] = true; } - // Confusing because the count might be inaccurate within a branch - //$this->propagateFields($locationList, '', 'overriddenVars', 'overriddenClass'); } - // Show ipxe menu - if (Module::isAvailable('serversetup') && class_exists('IPxe')) { - $res = Database::simpleQuery("SELECT ml.locationid, m.title, ml.defaultentryid FROM serversetup_menu m - INNER JOIN serversetup_menu_location ml USING (menuid) - WHERE locationid IN (:allowedLocationIds) GROUP BY locationid", compact('allowedLocationIds')); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $lid = (int)$row['locationid']; - if (isset($locationList[$lid])) { - if ($row['defaultentryid'] !== null) { - $row['title'] .= '(*)'; - } - $locationList[$lid]['customMenu'] = $row['title']; - } + unset($loc); + foreach ($plugins as $pk => $plugin) { + if ($plugin->propagateColumn()) { + self::propagateFields($locationList, $plugin, $pk); } - self::propagateFields($locationList, '', 'customMenu', 'customMenuClass'); } + foreach ($locationList as &$loc) { + $loc['plugins'] = array_values($loc['plugins']); + } + unset($loc); $addAllowedLocs = User::getAllowedLocations("location.add"); $addAllowedList = Location::getLocations(0, 0, true); @@ -224,44 +141,36 @@ class SubPage unset($loc); // Output - $data = array( + $data = [ + 'colspan' => (2 + count($plugins)), + 'plugins' => array_values($plugins), 'list' => array_values($locationList), - 'havestatistics' => Module::get('statistics') !== false, - 'havebaseconfig' => Module::get('baseconfig') !== false, - 'havesysconfig' => Module::get('sysconfig') !== false, - 'haveipxe' => Module::isAvailable('serversetup') && class_exists('IPxe'), 'overlapSelf' => $overlapSelf, 'overlapOther' => $overlapOther, 'mismatchMachines' => $mismatchMachines, - 'unassignedCount' => $unassigned, - 'unassignedLoad' => ($unassigned ? (round(($unassignedLoad / $unassigned) * 100)) : ''), - 'unassignedIdle' => ($unassigned ? (round((($unassignedLoad + $unassignedIdle) / $unassigned) * 100)) : ''), - 'unassignedOverrides' => $unassignedOverrides, - 'defaultConfig' => $defaultConfig, 'addAllowedList' => array_values($addAllowedList), - ); - // TODO: Buttons for config vars and sysconfig are currently always shown, as their availability - // depends on permissions in the according modules, not this one + ]; Permission::addGlobalTags($data['perms'], NULL, ['subnets.edit', 'location.add']); Render::addTemplate('locations', $data); Module::isAvailable('js_ip'); // For CIDR magic } - private static function propagateFields(&$locationList, $defaultValue, $name, $class) + private static function propagateFields(array &$locationList, AbstractLocationColumn $plugin, string $pluginKey) { $depth = array(); foreach ($locationList as &$loc) { $d = $loc['depth']; - if (!isset($loc[$name])) { + if (empty($loc['plugins'][$pluginKey]['html'])) { // Has no explicit config assignment - if ($d === 0) { - $loc[$name] = $defaultValue; - } else { - $loc[$name] = $depth[$d - 1]; - } - $loc[$class] = 'gray'; - } - $depth[$d] = $loc[$name]; + $loc['plugins'][$pluginKey]['html'] = $depth[$d - 1] ?? $plugin->propagateDefaultHtml(); + $loc['plugins'][$pluginKey]['class'] = 'gray'; + } elseif (empty($loc['plugins'][$pluginKey]['class'])) { + $loc['plugins'][$pluginKey]['class'] = 'slx-bold'; + $loc['plugins'][$pluginKey]['html'] = + $plugin->propagationOverride($depth[$d - 1] ?? $plugin->propagateDefaultHtml(), + $loc['plugins'][$pluginKey]['html']); + } + $depth[$d] = $loc['plugins'][$pluginKey]['html']; unset($depth[$d + 1]); } } diff --git a/modules-available/locations/pages/subnets.inc.php b/modules-available/locations/pages/subnets.inc.php index fb1e1e80..7628486b 100644 --- a/modules-available/locations/pages/subnets.inc.php +++ b/modules-available/locations/pages/subnets.inc.php @@ -3,7 +3,7 @@ class SubPage { - public static function doPreprocess($action) + public static function doPreprocess(string $action): bool { if ($action === 'updatesubnets') { self::updateSubnets(); @@ -42,7 +42,7 @@ class SubPage continue; } $range = LocationUtil::rangeToLongVerbose($start, $end); - if ($range === false) + if ($range === null) continue; list($startLong, $endLong) = $range; if ($stmt->execute(compact('startLong', 'endLong', 'loc', 'subnetid'))) { @@ -59,7 +59,7 @@ class SubPage Util::redirect('?do=Locations'); } - public static function doRender($getAction) + public static function doRender($getAction): bool { if ($getAction === false) { User::assertPermission('subnets.edit', NULL, '?do=locations'); @@ -67,7 +67,7 @@ class SubPage FROM subnet ORDER BY startaddr ASC, endaddr DESC"); $rows = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $row['startaddr'] = long2ip($row['startaddr']); $row['endaddr'] = long2ip($row['endaddr']); $row['locations'] = Location::getLocations($row['locationid']); @@ -81,7 +81,7 @@ class SubPage return false; } - public static function doAjax($action) + public static function doAjax($action): bool { return false; } |