* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP */ abstract class AdmissionRule { private static ?array $rules = null; /** * Reads all available AdmissionRule subclasses and loads their definitions. * * @param bool $activeOnly Show only active rules. */ public static function getAvailableAdmissionRules(bool $activeOnly = true): array { if (self::$rules === null) { self::$rules = []; $query = "SELECT * FROM `admissionrules` ORDER BY `id`"; DBManager::get()->fetchAll( $query, [], function ($row) { /** @var class-string $className */ $className = $row['ruletype']; $autoloadPath = $GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . $row['path']; if ( !class_exists($className) && is_dir($autoloadPath) && !StudipAutoloader::hasAutoloadPath($autoloadPath) ) { StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . $row['path']); } if (!is_subclass_of($className, self::class)) { return; } self::$rules[$className] = [ 'id' => $row['id'], 'name' => $className::getName(), 'description' => $className::getDescription(), 'active' => (bool) $row['active'], ]; } ); } if ($activeOnly) { return array_filter( self::$rules, fn($rule) => $rule['active'] ); } return self::$rules; } /** * @param class-string $name */ public static function getRule(string $name, ?string $id = null): ?AdmissionRule { $rules = self::getAvailableAdmissionRules(); if (!array_key_exists($name, $rules)) { throw new InvalidArgumentException("Rule '$name' does not exist."); } if (func_num_args() === 1 || $id === null) { return new $name(); } $rule = new $name($id); if ($rule->getId() !== $id) { return null; } return $rule; } // --- ATTRIBUTES --- /** * When does the validity end? */ public $endTime = 0; /** * A unique identifier for this rule. */ public $id = ''; /** * A customizable message that is shown to users that are rejected for admission * because of the current rule. */ public $message = ''; /** * default message that is shown to users that are rejected for admission * because of the current rule. */ public $default_message = ''; /** * When does the validity start? */ public $startTime = 0; /** * ID of the CourseSet this admission rule belongs to (is stored here for * performance reasons). */ public $courseSetId = ''; /** * courseset siblings of this rule */ public $siblings = []; /** * Are siblings set manually? */ public $siblings_override = false; /** * Is the admission rule template written in PHP or is it a VueJS component? * Valid values are 'php' and 'vue'. */ public string $type = 'php'; /** * If the template is a VueJS component, give the path here. */ public ?string $component = null; // --- OPERATIONS --- public function __construct($ruleId = '', $courseSetId = '') { $this->id = $ruleId; $this->courseSetId = $courseSetId; } /** * Hook that can be called after the seat distribution on the courseset * has completed. * * @param CourseSet $courseset Current courseset. */ public function afterSeatDistribution($courseset) { return true; } /** * Checks if we are in the rule validity time frame. * * @return bool True if the rule is valid because the time frame applies, * otherwise false. */ public function checkTimeFrame() { $valid = true; // Start time given, but still in the future. if ($this->startTime && $this->startTime > time()) { $valid = false; } // End time given, but already past. if ($this->endTime && $this->endTime < time()) { $valid = false; } return $valid; } /** * Deletes the admission rule and all associated data. */ public function delete() { // Delete rule assignment to coursesets. $stmt = DBManager::get()->prepare("DELETE FROM `courseset_rule` WHERE `rule_id`=?"); $stmt->execute([$this->id]); } /** * Generate a new unique ID. * * @param String $tableName */ public function generateId($tableName) { do { $newid = md5(uniqid(get_class($this).microtime(), true)); $db = DBManager::get()->query("SELECT `rule_id` FROM `".$tableName."` WHERE `rule_id`=" . DBManager::get()->quote($newid)); } while ($db->fetch()); return $newid; } /** * Gets all users that are matched by thís rule. * * @return Array An array containing IDs of users who are matched by * this rule. */ public function getAffectedUsers() { return []; } /** * Get end of validity. * * @return Integer */ public function getEndTime() { return $this->endTime; } /** * Subclasses of AdmissionRule can require additional data to be entered on * admission (like PasswordAdmission which needs a password for course * access). Their corresponding method getInput only returns a HTML form * fragment as the output can be concatenated with output from other * rules. * This static method provides the frame for rendering a full HTML form * around the fragments from subclasses. * * @return Array Start and end templates which wrap input form fragments * from subclasses. */ public static final function getInputFrame() { return [ $GLOBALS['template_factory']->open('admission/rules/input_start')->render(), $GLOBALS['template_factory']->open('admission/rules/input_end')->render() ]; } /** * Gets some text that describes what this AdmissionRule (or respective * subclass) does. */ public static function getDescription() { return _("Legt eine Regel fest, die erfüllt sein muss, um sich ". "erfolgreich zu einer Menge von Veranstaltungen anmelden zu ". "können."); } public function getInput() { return ''; } /** * Gets the rule ID. * * @return String This rule's ID. */ public function getId() { return $this->id; } /** * Gets the message that is shown to users rejected by this rule. * * @return String The message. */ public function getMessage() { return $this->message ?: $this->default_message; } /** * Return this rule's name. */ public static function getName() { return _("Anmelderegel"); } /** * Gets start of validity. * * @return Integer */ public function getStartTime() { return $this->startTime; } /** * Internal helper function for loading rule definition from database. */ public function load() { } /** * Hook that can be called when the seat distribution on the courseset * starts. * * @param CourseSet $courseset The courseset this rule belongs to. */ public function beforeSeatDistribution($courseset) { return true; } /** * Does the current rule allow the given user to register as participant * in the given course? * * @param String $userId * @param String $courseId * @return Array */ public function ruleApplies($userId, $courseId) { return []; } /** * Uses the given data to fill the object values. This can be used * as a generic function for storing data if the concrete rule type * isn't known in advance. * * @param Array $data * @return AdmissionRule This object. */ public function setAllData($data) { if (!empty($data['start_date']) && empty($data['start_time'])) { $data['start_time'] = strtotime($data['start_date']); } if (!empty($data['end_date']) && empty($data['end_time'])) { $data['end_time'] = strtotime($data['end_date'] . ' 23:59:59'); } $this->message = $data['message'] ?? ''; $this->startTime = $data['start_time'] ?? null; $this->endTime = $data['end_time'] ?? null; return $this; } /** * Sets a new end time for condition validity. * * @param Integer $newEndTime * @return AdmissionRule */ public function setEndTime($newEndTime) { $this->endTime = $newEndTime; return $this; } /** * Sets a new message to show to users. * * @param String $newMessage A new message text. * @return AdmissionRule This object */ public function setMessage($newMessage) { $this->message = $newMessage; return $this; } /** * Sets a new start time for condition validity. * * @param Integer $newStartTime * @return AdmissionRule */ public function setStartTime($newStartTime) { $this->startTime = $newStartTime; return $this; } /** * Helper function for storing rule definition to database. */ public function store() { } /** * A textual description of the current rule. * * @return String */ public function toString() { return ''; } /** * Validates if the given request data is sufficient to configure this rule * (e.g. if required values are present). * * @param Array $data Request data * @return Array Error messages. */ public function validate($data) { $errors = []; if (!empty($data['start_date']) && !empty($data['end_date']) && strtotime($data['end_date']) < strtotime($data['start_date'])) { $errors[] = _('Das Enddatum darf nicht vor dem Startdatum liegen.'); } return $errors; } /** * Standard string representation of this object. * * @return String */ public function __toString() { return $this->toString(); } /** * load sibling rules * */ public function loadSiblings() { if ($this->siblings_override) { return false; } $this->siblings = []; if ($this->courseSetId != '') { $cs = new CourseSet($this->courseSetId); foreach ($cs->getAdmissionRules() as $rule_id => $rule) { if ($rule->getId() != $this->id) { $this->siblings[$rule_id] = $rule; } } } } /** * get sibling rules * */ public function getSiblings() { $this->loadSiblings(); return $this->siblings; } /** * set sibling rules * */ public function setSiblings($siblings = []) { $this->siblings_override = true; $this->siblings = $siblings; } /** * checks if given admission rule is allowed to be combined with this rule * * @param AdmissionRule|string $admission_rule * @return boolean */ public function isCombinationAllowed($admission_rule) { if (is_object($admission_rule)) { $admission_rule = get_class($admission_rule); } return AdmissionRuleCompatibility::exists([get_class($this), $admission_rule]); } /** * Get fields and settings defining this admission rule as array. */ public function getPayload(): array { return [ 'start-time' => $this->startTime, 'end-time' => $this->endTime, 'message' => $this->message ?? $this->default_message ]; } public function __clone() { $this->id = md5(uniqid(get_class($this))); $this->courseSetId = null; } }