<?php /** * ModuleManagementModel.php * Parent class of all MVV-Models * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * @author Peter Thienel <thienel@data-quest.de> * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP * @since 3.5 */ require_once 'config/mvv_config.php'; abstract class ModuleManagementModel extends SimpleORMap implements ModuleManagementInterface { /** * Usable as option for ModuleManagementModel::getDisplayName(). * Use the deafault display options for this object. */ const DISPLAY_DEFAULT = 1; /** * Usable as option for ModuleManagementModel::getDisplayName(). * Displays semesters of the validity period if available for this object. */ const DISPLAY_SEMESTER = 2; /** * Usable as option for ModuleManagementModel::getDisplayName(). * Displays the code (usually a unique identifier) if available for this object. */ const DISPLAY_CODE = 4; /** * Usable as option for ModuleManagementModel::getDisplayName(). * Displays the name of the faculty if available for this object. */ const DISPLAY_FACULTY = 8; /** * Usable as option for ModuleManagementModel::getDisplayName(). * Displays the name of the Fach (subject of study) if available for this object. */ const DISPLAY_FACH = 16; /** * Usable as option for ModuleManagementModel::getDisplayName(). * Displays the name of the Studiengangteil if available for this object. */ const DISPLAY_STGTEIL = 32; /** * Usable as option ModuleManagementModel::getDisplayName(). * Displays the name of the Abschluss if available for this object. */ const DISPLAY_ABSCHLUSS = 64; /** * Usable as option ModuleManagementModel::getDisplayName(). * Displays the name of the Abschluss-Kategorie * if available for this object. */ const DISPLAY_KATEGORIE = 128; protected static $filter_params = []; protected $is_dirty = false; private static $language = null; protected static $perm_object = null; public $object_real_name = ''; protected static $object_cache = []; /** * Common configuration for all module management models. This will ensure * that the caches are flushed whenever an object changes or is removed. * * @param array $config Configuration from derived class */ protected static function configure($config = []) { $config['registered_callbacks']['after_store'][] = function () { self::clearCache(); }; $config['registered_callbacks']['after_delete'][] = function () { self::clearCache(); }; parent::configure($config); } /** * Returns a collection of a MVV object type found by search term optionally * filtered. * * @see ModuleManagementModel::getFilterSql() * @param string $search_term The term to search for. * @param array $filter Filter parameters as key value pairs. * @return SimpleORMapCollection A collection of "self" objects. */ public static function findBySearchTerm($search_term, $filter = null) { return new SimpleORMapCollection(); } /** * Returns an array of all objects of "self" type. * * @return array An array of "self" objects. */ public static function getAll() { return parent::findBySQL('1'); } /** * Returns an object by given id or a new object. * * @param string $id The id of the object. * @return ModuleManagementModel An object of "self" type. */ public static function get($id = null) { return static::findCached($id) ?? new static($id); } /** * Returns an object by given id with all relations and additional fields. * * @param string $id The id of the object. * @return ModuleManagementModel */ public static function getEnriched($id) { return parent::find($id); } /** * Verifies whether the given user has sufficient rights to create, modify * or delete this object and throws an exception if not. * * @param string $user_id The user's id. * @return boolean True if rights are sufficient * @throws Exception if rights are not sufficient. */ public function verifyPermission($user_id = null) { $user_id = $user_id ?: $GLOBALS['user']->id; $perm = MvvPerm::get($this); // PERM_CREATE means a permission to store a new one or delete one if ($this->isNew() || $this->isDeleted()) { if (!$perm->haveObjectPerm(MvvPerm::PERM_CREATE, $user_id)) { throw new Exception(sprintf( 'Permission denied! The user is not allowed to ' . 'create/delete an object of type %s.', static::class)); } } else { if (!$perm->haveObjectPerm(MvvPerm::PERM_WRITE, $user_id)) { throw new Exception(sprintf( 'Permission denied! The user is not allowed to store an ' . 'object of type %s', static::class)); } } // check the permissions for every single db field except primary keys if ($this->isNew()) { $fields = array_diff(array_keys($this->db_fields), array_values($this->pk)); } else { $fields = array_keys($this->db_fields); } foreach ($fields as $field) { if ($this->isFieldDirty($field) && !$perm->haveFieldPerm($field, MvvPerm::PERM_WRITE, $user_id)) { throw new Exception(sprintf( 'Permission denied! The user is not allowed to change ' . 'value of field %s.%s.', static::class, $field)); } } // check the permissions for every single relation foreach (array_keys($this->relations) as $relation) { $options = $this->getRelationOptions($relation); if ((isset($options['on_store']) || isset($options['on_delete'])) && ($options['type'] === 'has_one' || $options['type'] === 'has_many' || $options['type'] === 'has_and_belongs_to_many')) { if (isset($this->relations[$relation])) { if ($options['type'] === 'has_one') { $this->checkRelation($relation, $this->{$relation}, $perm, $user_id); } else { // datafields gets special treatment... if ($relation == 'datafields') { foreach ($this->datafields as $entry) { if ($entry->isNew() || $entry->isDirty()) { if (!$perm->haveDfEntryPerm($entry->datafield_id, MvvPerm::PERM_WRITE)) { throw new Exception(sprintf( 'Permission denied! The user is not ' . 'allowed to change value of field %s::datafields[%s] ("%s").', static::class, $entry->datafield_id, $entry->datafield->name)); } } } } else { foreach ($this->{$relation} as $r) { $this->checkRelation($relation, $r, $perm, $user_id); } } } } } } return true; } /** * Checks the rights for a relation. * * @param string $relation_name Field name of relation. * @param ModuleManagementModel $relation_object * @param int $perm * @param type $user_id * @return boolean * @throws Exception */ private function checkRelation($relation_name, $relation_object, $perm, $user_id) { if (($relation_object->isNew() || $relation_object->isDeleted()) && !$perm->haveFieldPerm($relation_name, MvvPerm::PERM_CREATE, $user_id)) { throw new Exception(sprintf( 'Permission denied! The user is not allowed to create/delete a relation %s::%s.', get_class($relation_object), $relation_name)); } elseif ($relation_object->isDirty()) { if ($relation_object instanceof ModuleManagementModel) { $relation_object->verifyPermission($user_id); } elseif (!$perm->haveFieldPerm($relation_name, MvvPerm::PERM_WRITE, $user_id)) { throw new Exception(sprintf( 'Permission denied! The user is not allowed to modify a relation %s::%s.', get_class($relation_object), $relation_name)); } } return true; } /** * @see SimpleOrMap::store() * Optional validation of values. Triggers logging of changes. * * @param boolean $validate True to validate values. */ public function store(/* $validate = true */) { $validate = true; if (func_num_args() > 0) { $validate = func_get_arg(0) !== false; } if ($validate) { $this->validate(); } if ($this->isNew() || $this->isDirty()) { $this->editor_id = $GLOBALS['user']->id; } $stored = false; if ($this->isNew()) { $this->author_id = $GLOBALS['user']->id; $stored = parent::store(); if ($stored) { $this->logChanges('new'); } } else { $this->logChanges('update'); $stored = parent::store(); } return $stored; } /** * Validates the values before store. Throws an InvalidValuesException * normally catched by form validation. * * @throws InvalidValuesException */ public function validate() { } /** * @see SimpleOrMap::delete() * Triggers logging. */ public function delete() { $this->logChanges('delete'); return parent::delete(); } /** * Logs all changes of this object. * * @param type $action new, update or delete * @return boolean Return true if logging was successful. */ protected function logChanges ($action = null) { switch ($this->db_table) { case 'abschluss' : $logging = 'MVV_ABSCHLUSS'; $num_index = 1; break; case 'mvv_abschl_kategorie' : $logging = 'MVV_KATEGORIE'; $num_index = 1; break; case 'mvv_abschl_zuord' : $logging = 'MVV_ABS_ZUORD'; $num_index = 2; break; case 'mvv_dokument' : $logging = 'MVV_DOKUMENT'; $num_index = 1; break; case 'mvv_dokument_zuord' : $logging = 'MVV_DOK_ZUORD'; $num_index = 3; break; case 'fach' : $logging = 'MVV_FACH'; $num_index = 1; break; case 'mvv_fachberater' : $logging = 'MVV_FACHBERATER'; $num_index = 2; break; case 'mvv_fach_inst' : $logging = 'MVV_FACHINST'; $num_index = 2; break; case 'mvv_lvgruppe' : $logging = 'MVV_LVGRUPPE'; $num_index = 1; break; case 'mvv_lvgruppe_modulteil' : $logging = 'MVV_LVMODULTEIL'; $num_index = 2; break; case 'mvv_lvgruppe_seminar' : $logging = 'MVV_LVSEMINAR'; $num_index = 2; break; case 'mvv_modul' : $logging = 'MVV_MODUL'; $num_index = 1; break; case 'mvv_modulteil' : $logging = 'MVV_MODULTEIL'; $num_index = 1; break; case 'mvv_modulteil_deskriptor' : $logging = 'MVV_MODULTEIL_DESK'; $num_index = 1; break; case 'mvv_modulteil_language' : $logging = 'MVV_MODULTEIL_LANG'; $num_index = 2; break; case 'mvv_modulteil_stgteilabschnitt' : $logging = 'MVV_MODULTEIL_STGTEILABS'; $num_index = 3; break; case 'mvv_modul_deskriptor' : $logging = 'MVV_MODUL_DESK'; $num_index = 1; break; case 'mvv_modul_inst' : $logging = 'MVV_MODULINST'; $num_index = 2; break; case 'mvv_modul_language' : $logging = 'MVV_MODUL_LANG'; $num_index = 2; break; case 'mvv_modul_user' : $logging = 'MVV_MODUL_USER'; $num_index = 3; break; case 'mvv_stgteil' : $logging = 'MVV_STGTEIL'; $num_index = 1; break; case 'mvv_stgteilabschnitt' : $logging = 'MVV_STGTEILABS'; $num_index = 1; break; case 'mvv_stgteilabschnitt_modul' : $logging = 'MVV_STGTEILABS_MODUL'; $num_index = 2; break; case 'mvv_stgteilversion' : $logging = 'MVV_STGTEILVERSION'; $num_index = 1; break; case 'mvv_stgteil_bez' : $logging = 'MVV_STGTEILBEZ'; $num_index = 1; break; case 'mvv_stg_stgteil' : $logging = 'MVV_STG_STGTEIL'; $num_index = 3; break; case 'mvv_studiengang' : $logging = 'MVV_STUDIENGANG'; $num_index = 1; break; default: return false; } if ($logging) { $aff = null; $coaff = null; $debuginfo =null; switch ($action) { case 'new': $logging .= '_NEW'; $debuginfo = $this->getDisplayName(); break; case 'update': $logging .= '_UPDATE'; break; case 'delete': $logging .= '_DEL'; $debuginfo = $this->getDisplayName(); break; default: return false; } $id_array = $this->getId(); switch ($num_index) { case '1': $aff = $id_array; break; case '2': $aff = $id_array[0]; $coaff = $id_array[1]; break; case '3': $aff = $id_array[0]; $coaff = $id_array[1]; $debuginfo = $id_array[2]; break; default: return false; } if ($action == 'update') { foreach ($this->content as $name => $value) { if ($name == 'author_id' || $name == 'editor_id' || $name == 'mkdate' || $name == 'chdate' ) continue; if ($this->isFieldDirty($name)) { $info = ($num_index == 3) ? $debuginfo.';'.$value : $value; StudipLog::log($logging, $aff, $coaff, $this->db_table.'.'.$name, $info, $editor_id); } } } else { StudipLog::log($logging, $aff, $coaff, $this->db_table, $debuginfo, $editor_id); } return true; } return false; } /** * Sets a new id for this object. */ public function setNewId() { $this->setId($this->getNewId()); } /** * Enriches the model with data from other joined tables. * * @param string $query complete sql with all fields in select statement * from main table * @param array $params Array with the parameters used in query * @param int $row_count Number of rows to return * @param int $offset Offset where the result set starts * @return SimpleOrMapCollection with all found objects or empty array */ public static function getEnrichedByQuery($query = null, $params = [], $row_count = null, $offset = null) { $enriched = []; $params = array_merge($params, self::$filter_params); self::$filter_params = []; if (!is_null($query)) { if (!is_null($row_count)) { $limit_sql = ' LIMIT ?'; $params[] = intval($row_count); if (!is_null($offset)) { $limit_sql .= ' OFFSET ?'; $params[] = intval($offset); } } else { $limit_sql = ''; } $stmt = DBManager::get()->prepare($query . $limit_sql); $stmt->execute($params); $model_object = new static(); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $data) { $pkey = []; foreach ($model_object->pk as $pk) { $pkey[]= $data[$model_object->db_fields[$pk]['name']]; } $data_object = clone $model_object; foreach ($data as $key => $value) { if (isset($data_object->db_fields[$key])) { $data_object->setValue($key, $value); } else { $data_object->content[mb_strtolower($key)] = $value; $data_object->content_db[mb_strtolower($key)] = $value; } } $data_object->setId($pkey); $data_object->setNew(false); $enriched[join('', $pkey)] = $data_object; } } return SimpleORMapCollection::createFromArray($enriched); } /** * Returns the name of the object to display in a specific context. The * default is the value from the db fields "name" or "bezeichnung" or an * empty string if no such fields exist. This method is overwritten by most * of the mvv objects to display more complex names glued together from * fields of related objects. * * @param mixed $options An optional parameter to set display options. * @return string The name for */ public function getDisplayName($options = self::DISPLAY_DEFAULT) { if ($this->isField('name')) { return (string) $this->getValue('name'); } if ($this->isField('bezeichnung')) { return (string) $this->getValue('bezeichnung'); } return ''; } /** * Returns the display name of this class. * * @return string the display name of this class */ public static function getClassDisplayName($long = false) { return 'Module Management Model'; } /** * Creates a sql clause to set constraints in a query by given filters. * * The values of the given filters can be either a scalar value or an array. * * If the value of the given filter is '__undefined__' then it matches * against NULL or an empty string. * * If the column name has * a comparison operator at its end (delimited by a blank), this operator * is used. * * To filter for semesters the column name has to be 'start_sem.beginn' for * the start semester and 'end_sem.ende' for the end semester according to * the joins with the semester_data table and table aliases in the sql * statement. * * @param array $filter An associative array with filters where the key is * the column name to filter against the given value. * @param bool $where if true returns a complete where statement * @param string SQL-where part glued with an "OR" at the end of the * filter sql part. * @return string The sql clause */ public static function getFilterSql($filter, $where = false, $or_sql = null) { $sql_parts = []; foreach ((array) $filter as $col => $val) { $col = trim($col); if (is_array($val)) { if (sizeof($val)) { $sql_parts[] = $col . ' IN(' . join(',', array_map( function ($val) { return DBManager::get()->quote($val); }, $val)) . ') '; } } else if (trim($val)) { if ($val == '__undefined__') { $sql_parts[] = '(ISNULL(' . $col . ') OR ' . $col . " = '')"; } else { if (preg_match('/([\w\.]+)\s+([\<\>\!]\=?)/', $col, $matches)) { $sql_parts[] = trim($matches[1]) . ' ' . $matches[2] . ' ' . DBManager::get()->quote($val) . ' '; } else if ($col == 'start_sem.beginn') { if ((int) $val >= 0) { // start semester filter for Module, Studiengaenge, ... $sql_parts[] = '(start_sem.beginn <= ' . DBManager::get()->quote($val) . ' OR ISNULL(start_sem.beginn))'; } } else if ($col == 'end_sem.ende') { if ((int) $val >= 0) { // end semester filter for Module, Studiengaenge, ... $sql_parts[] = '(end_sem.ende >= ' . DBManager::get()->quote($val) . ' OR ISNULL(end_sem.ende))'; } } else { $sql_parts[] = $col . ' = ' . DBManager::get()->quote($val) . ' '; } } } } $sql = implode(' AND ', $sql_parts); if (mb_strlen($sql)) { if ($or_sql) { $sql = '(' . $sql . ') OR (' . $or_sql . ')'; } $sql = $where ? ' WHERE (' . $sql . ') ' : ' AND (' . $sql . ') '; } return $sql; } /** * Verifies a field name or an array of field names if they are permitted for * sorting the result. If ok returns the given sort fields. If not, returns * the given standard_field or null. * * @param string|array $sort the fields to check * @param string $additional_fields additional allowed fields * @return string|null the verified sort fields */ protected static function checkSortFields($sort, $standard_field = null, $additional_fields = []) { if (!is_array($sort)) { $sort = explode(',', $sort); } $sorm = new static(); if (sizeof(array_intersect( array_merge(array_keys($sorm->db_fields), $additional_fields), $sort))) { return implode(',', $sort); } return $standard_field; } /** * Checks for valid fields and creates a sort statement for queries. * * @param string|array $sort The field(s) to sort by. * @param string $order The direction (ASC|DESC) * @param string $standard_field * @param array $additional_fields Calculated columns. * @return string The sort query part. */ protected static function createSortStatement($sort, $order = 'ASC', $standard_field = null, $additional_fields = []) { $order = mb_strtoupper(trim($order)) !== 'DESC' ? ' ASC' : ' DESC'; if (!is_array($sort)) { $sort = explode(',', $sort); } $sort = array_map('trim', $sort); $sorm_name = static::class; $sorm = new $sorm_name(); $allowed_fields = array_intersect( $sort, array_merge( array_keys($sorm->db_fields), $additional_fields ) ); if (count($allowed_fields) > 0) { return implode("{$order},", $allowed_fields) . $order; } return $standard_field; } /** * Returns an SimpleOrMap object as an Array. * Its like a static version of SimpleOrMap::toArray but * returns all content fields. Usefull as callback function. * * @param SimpleORMap $sorm The SimpleOrMap object to transform * @param bool $to_utf8 If true (default), the data will be utf8 transformed. * @return array The array with all content fields from object. */ public static function getContentArray(SimpleORMap $sorm, $to_utf8 = true) { return $sorm->contentToArray($to_utf8); } /** * Returns the number of objects of this type. Optionally reduced by * filter criteria. * * @param array An array with filter criteria. * See ApplicationSimpleORMap::getFilter(). * @return int The number of rows. */ public static function getCount($filter = null) { if ($filter) { $filter_sql = self::getFilterSql($filter, true); } else { $filter_sql = ''; } $sorm = new static(); $db = DBManager::get()->query('SELECT COUNT(*) FROM ' . $sorm->db_table . $filter_sql); return $db->fetchColumn(0); } /** * Returns the number of rows found by the given sql and filters. * * @param string $sql The sql query part. * @param array $filter An array of filters with respect to the query part. * @return int The number of rows. */ public static function getCountBySql($sql, $filter = null) { $stmt = DBManager::get()->prepare($sql . self::getFilterSql($filter, true)); $stmt->execute(self::$filter_params); return $stmt->fetchColumn(0); } /** * Sets the language for localized fields and the locale environment * globally. * Possible values are configured in mvv_config.php. * * @see mvv_config.php * @param string $language The language. */ public static final function setLanguage($language) { $language = mb_strtoupper(mb_strstr($language . '_', '_', true)); if (isset($GLOBALS['MVV_LANGUAGES']['values'][$language])) { $locale = $GLOBALS['MVV_LANGUAGES']['values'][$language]['locale']; setLocaleEnv($locale); self::setContentLanguage($language); // load config file again require $GLOBALS['STUDIP_BASE_PATH'] . '/config/mvv_config.php'; } } /** * Switches the content to the given language. * Compared to ModuleManagementModel::setLanguage() strings translated with * gettext are always in the prefered language selected by the user. * * @param string $language The language code (see mvv_config.php) */ public static function setContentLanguage($language) { if (!is_array($GLOBALS['MVV_LANGUAGES']['values'][$language])) { throw new InvalidArgumentException(); } $locale = $GLOBALS['MVV_LANGUAGES']['values'][$language]['locale']; I18NString::setContentLanguage($locale); self::$language = $language; } public function getAvailableTranslations() { $translations[] = $GLOBALS['MVV_LANGUAGES']['default']; $stmt = DBManager::get()->prepare('SELECT DISTINCT `lang` ' . 'FROM i18n ' . 'WHERE `object_id` = ? AND `table` = ?'); $stmt->execute([$this->id, $this->db_table]); foreach ($stmt->fetchAll() as $locale) { $language = mb_strtoupper(mb_strstr($locale['lang'], '_', true)); if (is_array($GLOBALS['MVV_LANGUAGES']['values'][$language])) { $translations[] = $language; } } return $translations; } /** * Returns the currently selected language. * * @return string The currently selected language. */ public static final function getLanguage() { $language = self::$language ?: $GLOBALS['MVV_LANGUAGES']['default']; return $language; } /** * Returns the suffix for ordinal numbers if the selected locale is EN or * a simple point if not. * * @param type $num * @return string The ordinal suffix or a point. */ public static function getLocaleOrdinalNumberSuffix($num) { if (ModuleManagementModel::getLanguage() == 'EN') { if ($num % 100 < 11 || $num % 100 > 13) { switch ($num % 10) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; } } return 'th'; } return '.'; } /** * Returns an array of all values for given class with status "public" * defined by configuration. * * @return array Array of defined values for status "public". */ public static function getPublicStatus($class_name = null) { $class_name = $class_name ?: static::class; $class_name = 'MVV_' . mb_strtoupper($class_name); $public_status = []; if (is_array($GLOBALS[$class_name]['STATUS']['values'])) { foreach ($GLOBALS[$class_name]['STATUS']['values'] as $key => $status) { if ($status['public']) { $public_status[] = $key; } } } return $public_status; } /** * Returns the status of this object. * Some MVV objects have a status declared in mvv_config.php. * * @return string|null The status or null if the object has no status. */ public function getStatus() { if ($this->isField('stat')) { return $this->stat; } return null; } /** * Returns whether this object has a public status. Public status means that * this object is public visible. The possible status are defined * in mvv_config.php. The set of possible status can be restricted by an * optional filter. Only the statis given in filter are checkrd. * * @param array $filter An array of status keys. * @return boolean True if object has an public status. */ public function hasPublicStatus($filter = null) { $public_status = ModuleManagementModel::getPublicStatus(static::class); $filtered_status = $filter ? array_intersect(words($filter), $public_status) : $public_status; $status = $this->getStatus(); return $status ? in_array($status, $filtered_status) : false; } public function getResponsibleInstitutes() { return []; } /** * Returns a string that identify a variant of this object. Returns an empty * string if no variant exists for this object. * * @return string String to identify a variant. */ public function getVariant() { return ''; } /** * Locates and returns an item from cache * * @param string $id Id of the item * @param string $index Optional index for the cache, defaults to class name * @return ModuleManagementModel object or null */ public static function findCached($id, $index = null) { // Prevent warnings if ($id === null || $id === false) { return null; } return self::fromCache($index ?? static::class, $id, function () use ($id) { return static::find($id); }); } /** * Clears the cache for a given index or completely. If this method is * called on this abstract class, it will always clear the whole cache. * Otherwise it will clear the cache for the subclass of this class or * the given index. * * @param mixed $index Optional index to clear */ public static function clearCache($index = null) { if (static::class === self::class && $index === null) { static::$object_cache = []; } else { $index = $index ?? static::class; unset(static::$object_cache[$index]); } } /** * Loads an item from cache and retrieves and stores it if it is not * present. * * @param string $index Index for the cache * @param string $id Id of the item * @param Callable $finder Function to actually locate the item. * @return ModuleManagementModel object or null */ protected static function fromCache($index, $id, Callable $finder) { // Leave immeditately if $id cannot be used as array index if (!is_string($id) && !is_int($id)) { return $finder(); } if (!isset(static::$object_cache[$index])) { static::$object_cache[$index] = []; } if (!array_key_exists($id, static::$object_cache[$index])) { static::$object_cache[$index][$id] = $finder(); } return static::$object_cache[$index][$id]; } }