aboutsummaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:07:19 +0200
committerJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:19:12 +0200
commita3da1483a9e689846179159355badfec8073dbec (patch)
tree770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /cli
current code from svn, revision 62608
Diffstat (limited to 'cli')
-rwxr-xr-xcli/antelope_to_barracuda.php107
-rwxr-xr-xcli/biest7783-fix.php137
-rwxr-xr-xcli/biest7789-fix.php95
-rwxr-xr-xcli/biest7866-fix.php47
-rwxr-xr-xcli/biest8136-fix.php31
-rwxr-xr-xcli/check-help-tours.php80
-rwxr-xr-xcli/cleanup_admission_rules.php49
-rw-r--r--cli/compatibility-rules/studip-4.0.php179
-rw-r--r--cli/compatibility-rules/studip-4.2.php17
-rw-r--r--cli/compatibility-rules/studip-4.4.php6
-rw-r--r--cli/compatibility-rules/studip-5.0.php60
-rwxr-xr-xcli/create_table_schemes.php47
-rwxr-xr-xcli/cronjob-worker.php34
-rwxr-xr-xcli/cronjobs.php54
-rwxr-xr-xcli/describe_models.php75
-rwxr-xr-xcli/dump_studip.php87
-rwxr-xr-xcli/extract-js-localizations.php204
-rwxr-xr-xcli/fix_collate.php24
-rwxr-xr-xcli/fix_endtime_weekly_recurred_events.php33
-rw-r--r--cli/getopts.php317
-rwxr-xr-xcli/help-translation-tool.php543
-rwxr-xr-xcli/i18n-plugin.php89
-rwxr-xr-xcli/kill_studip_user.php95
-rwxr-xr-xcli/migrate.php76
-rwxr-xr-xcli/migrate_help_content.php89
-rwxr-xr-xcli/myisam_to_innodb.php122
-rwxr-xr-xcli/plugin_manager306
-rwxr-xr-xcli/studip-compat.php203
-rw-r--r--cli/studip_cli_env.inc.php80
-rwxr-xr-xcli/tic_5671_scan.php172
-rw-r--r--cli/update-resource-booking-intervals.php33
-rwxr-xr-xcli/vue-gettext-split-translations.php16
32 files changed, 3507 insertions, 0 deletions
diff --git a/cli/antelope_to_barracuda.php b/cli/antelope_to_barracuda.php
new file mode 100755
index 0000000..4360ffd
--- /dev/null
+++ b/cli/antelope_to_barracuda.php
@@ -0,0 +1,107 @@
+#!/usr/bin/env php
+<?php
+require_once(__DIR__.'/studip_cli_env.inc.php');
+
+echo 'Migration starting at '.date('d.m.Y H:i:s').".\n";
+$start = microtime(true);
+
+global $DB_STUDIP_DATABASE;
+
+// Tables to ignore on engine conversion.
+$ignore_tables = [];
+
+// Check if InnoDB is enabled in database server.
+$engines = DBManager::get()->fetchAll("SHOW ENGINES");
+$innodb = false;
+foreach ($engines as $e) {
+ // InnoDB is found and enabled.
+ if ($e['Engine'] == 'InnoDB' && in_array(mb_strtolower($e['Support']), ['default', 'yes'])) {
+ $innodb = true;
+ break;
+ }
+}
+
+if ($innodb) {
+ // Get version of database system (MySQL/MariaDB/Percona)
+ $data = DBManager::get()->fetchFirst("SELECT VERSION() AS version");
+ $version = $data[0];
+
+ // Use Barracuda format if database supports it (5.5 upwards).
+ if (version_compare($version, '5.5', '>=')) {
+ echo "\tChecking if Barracuda file format is supported...";
+ // Get innodb_file_per_table setting
+ $data = DBManager::get()->fetchOne("SHOW VARIABLES LIKE 'innodb_file_per_table'");
+ $file_per_table = $data['Value'];
+
+ // Check if Barracuda file format is enabled
+ $data = DBManager::get()->fetchOne("SHOW VARIABLES LIKE 'innodb_file_format'");
+ $file_format = $data['Value'];
+
+ if (mb_strtolower($file_per_table) == 'on' && mb_strtolower($file_format) == 'barracuda') {
+
+ echo " yes.\n";
+
+ // Fetch all tables that need to be converted.
+ $tables = DBManager::get()->fetchFirst("SELECT TABLE_NAME
+ FROM `information_schema`.TABLES
+ WHERE TABLE_SCHEMA=:database AND ENGINE=:engine
+ AND ROW_FORMAT IN (:rowformats)
+ ORDER BY TABLE_NAME",
+ [
+ ':database' => $DB_STUDIP_DATABASE,
+ ':engine' => 'InnoDB',
+ ':rowformats' => ['Compact', 'Redundant']
+ ]);
+
+ $newformat = 'DYNAMIC';
+
+ // Prepare query for table conversion.
+ $stmt = DBManager::get()->prepare("ALTER TABLE :database.:table ROW_FORMAT=:newformat");
+ $stmt->bindParam(':database', $DB_STUDIP_DATABASE, StudipPDO::PARAM_COLUMN);
+ $stmt->bindParam(':newformat', $newformat, StudipPDO::PARAM_COLUMN);
+
+ if (count($tables) > 0) {
+
+ // Now convert the found tables.
+ foreach ($tables as $t) {
+ $local_start = microtime(true);
+ $stmt->bindParam(':table', $t, StudipPDO::PARAM_COLUMN);
+ $stmt->execute();
+ $local_end = microtime(true);
+ $local_duration = $local_end - $local_start;
+ $human_local_duration = sprintf("%02d:%02d:%02d",
+ ($local_duration / 60 / 60) % 24, ($local_duration / 60) % 60, $local_duration % 60);
+
+ echo "\tConversion of table " . $t . " took " . $human_local_duration . ".\n";
+ }
+
+ } else {
+ echo "\tNo Antelope format tables found.\n";
+ }
+
+ } else {
+ echo " no:\n";
+ if (mb_strtolower($file_per_table) != 'on') {
+ echo "\t- file_per_table not set\n";
+ }
+ if (mb_strtolower($file_format) != 'barracuda') {
+ echo "\t- file_format not set to Barracuda (but to " . $file_format . ")\n";
+ }
+ }
+
+ $end = microtime(true);
+
+ $duration = $end - $start;
+ $human_duration = sprintf("%02d:%02d:%02d",
+ ($duration / 60 / 60) % 24, ($duration / 60) % 60, $duration % 60);
+
+ echo 'Migration finished at ' . date('d.m.Y H:i:s') . ', duration ' . $human_duration . ".\n";
+
+ } else {
+ echo "Your database server does not yet support the Barracuda row format (you need at least 5.5).\n";
+ }
+
+} else {
+ echo "The storage engine InnoDB is not enabled in your ".
+ "database installation, tables cannot be converted.\n";
+}
diff --git a/cli/biest7783-fix.php b/cli/biest7783-fix.php
new file mode 100755
index 0000000..e3cad62
--- /dev/null
+++ b/cli/biest7783-fix.php
@@ -0,0 +1,137 @@
+#!/usr/bin/env php
+<?php
+/**
+ * This script removes all members from a course that should not have been
+ * members in the first place.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @see https://develop.studip.de/trac/ticket/7783
+ */
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+require_once __DIR__ . '/../config/config_local.inc.php';
+
+function output($what) {
+ if (StudipVersion::olderThan(4)) {
+ $what = studip_utf8encode($what);
+ }
+
+ fwrite(STDOUT, $what);
+}
+
+$opts = getopt('d', ['dry-run']);
+$dry_run = isset($opts['d']) || isset($opts['dry-run']);
+
+// Reduce arguments by options (this is far from perfect)
+$args = $_SERVER['argv'];
+$arg_stop = array_search('--', $args);
+if ($arg_stop !== false) {
+ $args = array_slice($args, $arg_stop + 1);
+} elseif (count($opts)) {
+ $args = array_slice($args, 1 + count($opts));
+} else {
+ $args = array_slice($args, 1);
+}
+
+if (count($args) < 1) {
+ output("Fix for Biest 7783 - Use {$argv[0]} [--dry-run/-d] <semester_id,current,next>\n");
+ exit(0);
+}
+
+$semester_ids = explode(',', implode(',', array_map('trim', $args)));
+foreach ($semester_ids as $index => $semester_id) {
+ if ($semester_id === 'current') {
+ $semester_id = Semester::findCurrent()->id;
+ } elseif ($semester_id === 'next') {
+ $semester_id = Semester::findNext()->id;
+ } elseif (Semester::find($semester_id) === null) {
+ output("Semester id {$semester_id} is invalid\n");
+ exit(0);
+ }
+
+ $semester_ids[$index] = $semester_id;
+}
+
+$query = "SELECT DISTINCT
+ cs.`set_id`, s.`seminar_id`
+ FROM `semester_data` AS sd
+ JOIN `seminare` AS s
+ ON (s.`start_time` <= sd.`beginn`
+ AND (
+ sd.`beginn` <= s.`start_time` + s.`duration_time`
+ OR s.`duration_time` = -1
+ )
+ )
+ JOIN `seminar_courseset` AS scs USING (`seminar_id`)
+ JOIN `coursesets` AS cs USING (`set_id`)
+ JOIN `auth_user_md5` USING (`user_id`)
+ JOIN `courseset_rule` AS csr USING (`set_id`)
+ JOIN `admission_condition` AS ac USING (`rule_id`)
+ JOIN `userfilter` AS uf USING (`filter_id`)
+ JOIN `userfilter_fields` AS uff USING (`filter_id`)
+ WHERE `semester_id` IN (:semester_ids)
+ AND `algorithm_run` = 0
+ AND uff.`type` = 'SemesterOfStudyCondition'
+ AND uff.`value` > 1
+ ORDER BY cs.`name` ASC, s.`name`";
+$statement = DBManager::get()->prepare($query);
+$statement->bindValue(':semester_ids', $semester_ids);
+$statement->execute();
+$sets = $statement->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_COLUMN);
+
+foreach ($sets as $set_id => $course_ids) {
+ $courseset = new CourseSet($set_id);
+ $remove = [];
+ foreach ($course_ids as $course_id) {
+ $course = Course::find($course_id);
+ $members = new MembersModel($course_id, $course->getFullname());
+ $applicants = $members->getAdmissionMembers();
+ foreach (['awaiting', 'claiming'] as $status) {
+ foreach ($applicants[$status] as $applicant) {
+ $errors = $courseset->checkAdmission($applicant->user_id, $course_id);
+ if (count($errors) === 0) {
+ continue;
+ }
+
+ if (!isset($remove[$course_id])) {
+ $remove[$course_id] = [
+ 'course' => $course,
+ 'members' => $members,
+ 'status' => [],
+ ];
+ }
+ if (!isset($remove[$course_id]['status'][$status])) {
+ $remove[$course_id]['status'][$status] = [];
+ }
+
+ $remove[$course_id]['status'][$status][] = User::find($applicant['user_id']);
+ }
+ }
+ }
+
+ if ($remove) {
+ $owner = User::find($courseset->getUserId())->getFullname();
+ output("= Anmeldeset {$courseset->getName()} ({$owner}):\n");
+
+ foreach ($remove as $row) {
+ output(" - Veranstaltung {$row['course']->getFullname()}:\n");
+ foreach ($row['status'] as $status => $users) {
+ $user_ids = array_map(function (User $user) {
+ return $user->id;
+ }, $users);
+
+ if ($dry_run) {
+ foreach ($users as $user) {
+ output(" - Nutzer {$user->getFullname()}\n");
+ }
+ } else {
+ $result = $row['members']->cancelAdmissionSubscription($user_ids, $status);
+ foreach ($result as $row) {
+ output(" - Nutzer {$row}\n");
+ }
+ }
+
+ }
+ }
+ }
+}
diff --git a/cli/biest7789-fix.php b/cli/biest7789-fix.php
new file mode 100755
index 0000000..e94a9f3
--- /dev/null
+++ b/cli/biest7789-fix.php
@@ -0,0 +1,95 @@
+#!/usr/bin/env php
+<?php
+/**
+ * This script converts selected database columns from php serialization to json
+ *
+ * @author Till Glöggler <studip@tillgloeggler.de>
+ * @see https://develop.studip.de/trac/ticket/7789
+ */
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+require_once __DIR__ . '/../config/config_local.inc.php';
+
+ini_set('default_charset', 'utf-8');
+
+function legacy_studip_utf8encode($data)
+{
+ if (is_array($data)) {
+ $new_data = [];
+ foreach ($data as $key => $value) {
+ $key = legacy_studip_utf8encode($key);
+ $new_data[$key] = legacy_studip_utf8encode($value);
+ }
+ return $new_data;
+ }
+
+ if (!preg_match('/[\200-\377]/', $data) && !preg_match("'&#[0-9]+;'", $data)) {
+ return $data;
+ } else {
+ return mb_decode_numericentity(
+ mb_convert_encoding($data,'UTF-8', 'WINDOWS-1252'),
+ [0x100, 0xffff, 0, 0xffff],
+ 'UTF-8'
+ );
+ }
+}
+
+
+function convert_to_json($table, $column, $where = null)
+{
+ $db = DBManager::get();
+
+ echo "\n\n /*************************************************\n";
+ echo " ***** " . $table ." ***** ";
+ echo "\n *************************************************/\n\n";
+
+ // get primary keys
+ $result = $db->query("SHOW KEYS FROM $table WHERE Key_name = 'PRIMARY'");
+ $keys = [];
+
+ while ($data = $result->fetch(PDO::FETCH_ASSOC)) {
+ $keys[] = $data['Column_name'];
+ }
+
+ // retrieve and convert data
+ $result = $db->query("SELECT `". implode('`,`', $keys) ."`, `$column` FROM `$table` WHERE ". ($where ?: '1'));
+
+ while ($data = $result->fetch(PDO::FETCH_ASSOC)) {
+ $content = unserialize(legacy_studip_utf8decode($data[$column]));
+
+ if ($content === false) {
+ // try to fix string length denotations
+ $fixed = preg_replace_callback(
+ '/s:([0-9]+):\"(.*?)\";/s',
+ function ($matches) { return "s:".strlen($matches[2]).':"'.$matches[2].'";'; },
+ $data[$column]
+ );
+
+ $content = unserialize(legacy_studip_utf8decode($fixed));
+ }
+
+ if ($content !== false) {
+ // encode all data
+ $json = json_encode(legacy_studip_utf8encode($content), true);
+
+ $query = "UPDATE `$table` SET `$column` = ". $db->quote($json) ."\n WHERE ";
+
+ $where_query = [];
+ foreach ($keys as $key) {
+ $where_query[] = "`$key` = ". $db->quote($data[$key]);
+ }
+
+ $q = $query . implode(' AND ', $where_query);
+ $db->exec($q);
+ echo $q .";\n";
+ } else {
+ echo '/* Could not convert: '. print_r($data, 1) ." */\n";
+ }
+ }
+}
+
+convert_to_json('extern_config', 'config');
+convert_to_json('aux_lock_rules', 'attributes');
+convert_to_json('aux_lock_rules', 'sorting');
+convert_to_json('user_config', 'value', "field = 'MY_COURSES_ADMIN_VIEW_FILTER_ARGS'");
+convert_to_json('mail_queue_entries', 'mail');
diff --git a/cli/biest7866-fix.php b/cli/biest7866-fix.php
new file mode 100755
index 0000000..ca1f90c
--- /dev/null
+++ b/cli/biest7866-fix.php
@@ -0,0 +1,47 @@
+#!/usr/bin/env php
+<?php
+/**
+ * This script sets folder range_ids to the range_ids of their parent folder.
+ *
+ * @author Thomas Hackl <thomas.hackl@uni-passau.de>
+ * @see https://develop.studip.de/trac/ticket/7866
+ */
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+
+/**
+ * Sets the range_id of all child folders to the given range_id.
+ * @param $parent_folder
+ * @param $range_id
+ */
+function setFolderRangeId($parent_folder, $range_id) {
+ // Update all child folder range_ids.
+ DBManager::get()->execute(
+ "UPDATE `folders` SET `range_id` = :range WHERE `parent_id` = :parent",
+ [
+ 'range' => $range_id,
+ 'parent' => $parent_folder
+ ]
+ );
+
+ // Recursion: set correct range_id for child folders with wrong range_id.
+ $children = DBManager::get()->fetchAll(
+ "SELECT `id`, `range_id` FROM `folders` WHERE `parent_id` = :parent",
+ [
+ 'parent' => $parent_folder
+ ]
+ );
+ foreach ($children as $child) {
+ if ($child['range_id'] != $range_id) {
+ echo sprintf("Folder %s -> range_id %s.\n", $child['id'], $range_id);
+ }
+ setFolderRangeId($child['id'], $range_id);
+ }
+}
+
+// Fetch all root folders and process their children recursively.
+$root_folders = DBManager::get()->fetchAll("SELECT `id`, `range_id` FROM `folders` WHERE `parent_id` = ''");
+
+foreach ($root_folders as $r) {
+ setFolderRangeId($r['id'], $r['range_id']);
+}
diff --git a/cli/biest8136-fix.php b/cli/biest8136-fix.php
new file mode 100755
index 0000000..c1bad9a
--- /dev/null
+++ b/cli/biest8136-fix.php
@@ -0,0 +1,31 @@
+#!/usr/bin/env php
+<?php
+/**
+ * This script adjusts all activities so that anonymous posts will actually be
+ * anonymous.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @see https://develop.studip.de/trac/ticket/8136
+ */
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+require_once __DIR__ . '/../config/config_local.inc.php';
+
+$query = "UPDATE `activities`
+ SET `actor_type` = 'anonymous',
+ `actor_id` = ''
+ WHERE `provider` = :provider
+ AND `actor_type` != 'anonymous'
+ AND `object_id` IN (
+ SELECT `topic_id`
+ FROM `forum_entries`
+ WHERE `anonymous` != 0
+ )";
+$statement = DBManager::get()->prepare($query);
+$statement->bindValue(':provider', 'Studip\\Activity\\ForumProvider');
+$statement->execute();
+
+printf(
+ "%u forum post activities were anonymized\n",
+ $statement->rowCount()
+);
diff --git a/cli/check-help-tours.php b/cli/check-help-tours.php
new file mode 100755
index 0000000..e028621
--- /dev/null
+++ b/cli/check-help-tours.php
@@ -0,0 +1,80 @@
+#!/usr/bin/env php
+<?php
+/**
+ * This script will check whether the help tours steps are still valid
+ * regarding the controllers and actions.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ */
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+require_once __DIR__ . '/../config/config_local.inc.php';
+
+foreach (HelpTour::findBySQL('1 ORDER BY name ASC') as $tour) {
+ if (!$tour->settings->active) {
+ continue;
+ }
+
+ $errors = [];
+ foreach ($tour->steps->orderBy('step ASC') as $step) {
+ try {
+ if (strpos($step->route, 'plugins.php') === 0) {
+ $result = PluginEngine::routeRequest(substr($step->route, strlen('plugins.php') + 1));
+
+ // retrieve corresponding plugin info
+ $plugin_manager = PluginManager::getInstance();
+ $plugin_info = $plugin_manager->getPluginInfo($result[0]);
+
+ $file = implode('/', [
+// $GLOBALS['ABSOLUTE_PATH_STUDIP'],
+ Config::get()->PLUGINS_PATH,
+ $plugin_info['path'],
+ $plugin_info['class'],
+ ]);
+
+ if (file_exists($file . '.php')) {
+ $file .= '.php';
+ } elseif (file_exists($file . '.class.php')) {
+ $file .= '.class.php';
+ } else {
+ throw new Exception();
+ }
+ require_once $file;
+ $plugin = new $plugin_info['class'];
+
+ if ($result[1]) {
+ $dispatcher = new Trails_Dispatcher(
+ $GLOBALS['ABSOLUTE_PATH_STUDIP'] . $plugin->getPluginPath(),
+ rtrim(PluginEngine::getLink($plugin, [], null, true), '/'),
+ 'index'
+ );
+ $dispatcher->current_plugin = $plugin;
+ $parsed = $dispatcher->parse($result[1]);
+ $controller = $dispatcher->load_controller($parsed[0]);
+ if ($parsed[1] && !$controller->has_action($parsed[1])) {
+ throw new Exception();
+ }
+ }
+ } elseif (strpos($step->route, 'dispatch.php') === 0) {
+ $dispatcher = new StudipDispatcher();
+ $parsed = $dispatcher->parse(substr($step->route, strlen('dispatch.php') + 1));
+ $controller = $dispatcher->load_controller($parsed[0]);
+ if ($parsed[1] && !$controller->has_action($parsed[1])) {
+ throw new Exception();
+ }
+ } elseif (!file_exists("{$GLOBALS['ABSOLUTE_PATH_STUDIP']}{$step->route}")) {
+ throw new Exception();
+ }
+ } catch (Exception $e) {
+ $errors[$step->step] = $step->route;
+ }
+ }
+
+ if ($errors) {
+ $type = ucfirst($tour->type);
+ echo "{$type} '{$tour->name}' has errors in the following steps:\n";
+ foreach ($errors as $step => $route) {
+ echo "- Step {$step}: {$route}\n";
+ }
+ }
+}
diff --git a/cli/cleanup_admission_rules.php b/cli/cleanup_admission_rules.php
new file mode 100755
index 0000000..49998ff
--- /dev/null
+++ b/cli/cleanup_admission_rules.php
@@ -0,0 +1,49 @@
+#!/usr/bin/env php
+<?php
+/**
+ * cleanup_admission_rules.php
+ *
+ * deletes entries in %admissions tables
+ * which were orphaned by BIEST #6617
+ *
+ * @author André Noack <noack@data-quest.de>
+ * @license GPL2 or any later version
+ * @copyright Stud.IP Core Group
+ */
+require_once 'studip_cli_env.inc.php';
+require_once 'lib/classes/admission/CourseSet.class.php';
+
+$sql = "SELECT * FROM
+(
+SELECT rule_id,'ConditionalAdmission' as class FROM `conditionaladmissions`
+UNION
+SELECT rule_id,'CourseMemberAdmission' as class FROM `coursememberadmissions`
+UNION
+SELECT rule_id,'LimitedAdmission' as class FROM limitedadmissions
+UNION
+SELECT rule_id,'LockedAdmission' as class FROM lockedadmissions
+UNION
+SELECT rule_id,'ParticipantRestrictedAdmission' as class FROM participantrestrictedadmissions
+UNION
+SELECT rule_id,'PasswordAdmission' as class FROM passwordadmissions
+UNION
+SELECT rule_id,'TimedAdmission' as class FROM timedadmissions
+) a
+LEFT JOIN courseset_rule USING(rule_id) WHERE set_id IS NULL";
+
+$foo = new CourseSet();
+$c1 = $c2 = 0;
+DBManager::get()
+->fetchAll($sql, null, function ($data) use (&$c1,&$c2) {
+ $c1++;
+ if (class_exists($data['class'])) {
+ $rule = new $data['class']($data['rule_id']);
+ if ($rule->getId() === $data['rule_id']) {
+ echo 'deleting: ' . $rule->getName() . ' with id: ' . $rule->getId() . chr(10);
+ $c2++;
+ $rule->delete();
+ }
+ }
+}
+);
+printf("found: %s deleted: %s \n", $c1,$c2);
diff --git a/cli/compatibility-rules/studip-4.0.php b/cli/compatibility-rules/studip-4.0.php
new file mode 100644
index 0000000..70475b8
--- /dev/null
+++ b/cli/compatibility-rules/studip-4.0.php
@@ -0,0 +1,179 @@
+<?php
+// "Rules"/definitions for critical changes in 4.0
+return [
+ 'cssClassSwitcher' => 'Remove completely, use #{yellow:<table class="default">} instead.',
+ '$csssw' => '[#{cyan:cssClassSwitcher}] Remove completely, use #{yellow:<table class="default">} instead.',
+
+ 'DBMigration' => 'Use #{yellow:Migration} instead',
+
+ 'Request::removeMagicQuotes()' => 'Remove completely since magic quotes are removed from php',
+
+ 'base_without_infobox' => 'Use #{yellow:layouts/base.php} instead.',
+ 'deprecated_tabs_layout' => 'Don\'t use this. Use the global layout #{yellow:layouts/base.php} and #{yellow:Navigation} instead.',
+ 'setInfoBoxImage' => 'Replace with #{yellow:Sidebar}',
+ 'addToInfobox' => 'Replace with #{yellow:Sidebar}',
+ 'InfoboxElement' => 'Replace with appropriate #{yellow:Sidebar} element',
+ 'InfoboxWidget' => 'Replace with appropriate #{yellow:Sidebar} widget',
+
+ 'details.php' => 'Link to #{yellow:dispatch.php/course/details} instead',
+ 'institut_main.php' => 'Link to #{yellow:dispatch.php/institute/overview} instead',
+ 'meine_seminare.php' => 'Link to #{yellow:dispatch.php/my_courses} instead',
+ 'sms_box.php' => 'Link to #{yellow:dispatch.php/messages/overview} or #{yellow:dispatch.php/messages/sent} instead',
+ 'sms_send.php' => 'Link to #{yellow:dispatch.php/messages/write} instead',
+
+ 'get_global_perm' => 'Use #{yellow:$GLOBALS[\'perm\']->get_perm()} instead',
+ 'log_event(' => 'Use #{yellow:StudipLog::log()} instead',
+ '->removeOutRangedSingleDates' => 'Use #{yellow:SeminarCycleDate::removeOutRangedSingleDates} instead',
+
+ 'HolidayData' => 'Use class #{yellow:SemesterHoliday} instead',
+
+ 'CourseTopic::createFolder' => 'Use #{yellow:CourseTopic::connectWithDocumentFolder()} instead',
+ 'SimpleORMap::haveData' => 'Use #{yellow:SimpleORMap::isDirty()} or #{yellow:SimpleORMap::isNew()} instead',
+ 'Seminar::getMetaDateType' => 'Don\'t use this!',
+ 'UserConfig::setUserId' => 'Don\'t use this. #{yellow:Set the user via the constructor}.',
+
+ 'StudIPTemplateEngine' => 'Time to refactor your plugin.',
+ 'AbstractStudIPAdministrationPlugin' => 'Time to refactor your plugin.',
+ 'AbstractStudIPCorePlugin' => 'Time to refactor your plugin.',
+ 'AbstractStudIPHomepagePlugin' => 'Time to refactor your plugin.',
+ 'AbstractStudIPLegacyPlugin' => 'Time to refactor your plugin.',
+ 'AbstractStudIPPortalPlugin' => 'Time to refactor your plugin.',
+ 'AbstractStudIPStandardPlugin' => 'Time to refactor your plugin.',
+ 'AbstractStudIPSystemPlugin' => 'Time to refactor your plugin.',
+ 'new Permission(' => 'Time to refactor your plugin.',
+ 'Permission::' => 'Time to refactor your plugin.',
+ 'PluginNavigation' => 'Time to refactor your plugin.',
+ 'new StudIPUser(' => 'Time to refactor your plugin.',
+ 'StudIPUser::' => 'Time to refactor your plugin.',
+ 'StudipPluginNavigation' => 'Time to refactor your plugin.',
+ 'getLinkToAdministrationPlugin' => 'Time to refactor your plugin.',
+ 'getCurrentPluginId' => 'Time to refactor your plugin.',
+ 'saveToSession' => 'Time to refactor your plugin.',
+ 'getValueFromSession' => 'Time to refactor your plugin.',
+
+ 'ContainerTable' => false,
+ 'DbCrossTableView' => false,
+ 'DbPermissions' => false,
+
+ 'pclzip' => 'Use #{yellow:Studip\\ZipArchive} instead',
+ 'get_global_visibility_by_id' => 'Use #{yellow:User::visible} attribute instead',
+
+ 'getSeminarRoomRequest' => 'Use #{yellow:RoomRequest} model instead',
+ 'getDateRoomRequest' => 'Use #{yellow:RoomRequest} model instead',
+
+ 'ldate' => 'Use PHP\'s #{yellow:date()} or #{yellow:strftime()} function instead',
+ 'day_diff' => 'Use PHP\'s #{yellow:DateTime::diff()} method instead',
+ 'get_day_name' => 'Use PHP\'s #{yellow:strftime()} function with #{yellow:parameter \'%A\'} instead',
+ 'wday(' => 'Use #{strftime("%a")} or #{strftime("%A")} instead',
+
+ 'get_ampel_state' => false,
+ 'get_ampel_write' => false,
+ 'get_ampel_read' => false,
+ 'localePictureUrl' => false,
+ 'localeUrl' => false,
+ 'isDatesMultiSem' => false,
+ 'getMetadateCorrespondingDates' => false,
+ 'getCorrespondingMetadates' => false,
+ 'create_year_view' => false,
+ 'javascript_hover_year' => false,
+ 'js_hover' => false,
+ 'info_icons' => false,
+
+ 'get_message_attachments' => 'Use #{yellow:Message::attachments} attribute instead',
+ 'view_turnus' => 'Use #{yellow:Seminar::getFormattedTurnus()} instead',
+
+ 'AddNewStatusgruppe' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'CheckSelfAssign' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'CheckSelfAssignAll' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'CheckAssignRights' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'SetSelfAssignAll' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'SetSelfAssignExclusive' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'EditStatusgruppe' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'MovePersonPosition' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'SortPersonInAfter' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'SortStatusgruppe' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'SubSortStatusgruppe' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'resortStatusgruppeByRangeId' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'SwapStatusgruppe' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'CheckStatusgruppe' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'GetRangeOfStatusgruppe' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'GetGroupsByCourseAndUser' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'getOptionsOfStGroups' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'setOptionsOfStGroup' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'GetStatusgruppeLimit' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'CheckStatusgruppeFolder' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'CheckStatusgruppeMultipleAssigns' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'sortStatusgruppeByName' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'getPersons(' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'getSearchResults(' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+ 'setExternDefaultForUser' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+
+ 'GetStatusgruppeName' => 'Use #{yellow:Statusgruppen::find($id)->name} instead',
+ 'GetStatusgruppenForUser' => 'Use class #{yellow:Statusgruppe} or model #{yellow:Statusgruppen} instead (yupp, this is still pretty fucked up).',
+
+ 'get_global_visibility_by_id' => 'Use #{yellow:User::find($id)->visible} instead',
+ 'get_global_visibility_by_username' => 'Use #{yellow:User::findByUsername($username)->visible} instead',
+
+ 'get_local_visibility_by_username' => false,
+ 'get_homepage_element_visibility' => false,
+ 'set_homepage_element_visibility' => false,
+ 'checkVisibility' => 'Use #{yellow:Visibility::verify($param, $this->current_user->user_id)} instead',
+
+ 'InsertPersonStatusgruppe' => 'Use #{Statusgruppen::addUser()} instead',
+ 'RemovePersonStatusgruppe(' => 'Use #{yellow:Statusgruppen::find($group_id)->removeUser($user_id)} instead',
+ 'RemovePersonStatusgruppeComplete' => 'Use #{yellow:Statusgruppen::find($group_id)->removeUser($user_id, true)} instead. Maybe you will need to do this on a collection of groups for a course or institute.',
+ 'RemovePersonFromAllStatusgruppen' => 'Use #{yellow:StatusgruppeUser::deleteBySQL("user_id = ?", [$user_id])} instead.',
+ 'DeleteAllStatusgruppen' => 'Use #{yellow:Statusgruppen::deleteBySQL("range_id = ?", [$id]);} instead',
+ 'DeleteStatusgruppe' => 'Use #{yellow:Statusgruppen::delete()} - or #{yellow:Statusgruppen::remove()} if you want to keep the child groups.',
+ 'moveStatusgruppe' => false,
+ 'CheckUserStatusgruppe' => 'Use #{yellow:StatusgruppeUser::exists([$group_id, $user_id])} instead.',
+ 'CountMembersStatusgruppen' => false,
+ 'CountMembersPerStatusgruppe' => false,
+ 'MakeDatafieldsDefault' => 'No longer neccessary.',
+ 'MakeUniqueStatusgruppeID' => 'No longer neccessary. SORM will create ids for you.',
+ 'GetAllSelected' => 'Use #{yellow:Statusgruppen::findAllByRangeId()} instead.',
+ 'getStatusgruppenIDS' => 'Use #{yellow:Statusgruppen::findByRange_id()} instead.',
+ 'getAllStatusgruppenIDS' => 'Use #{yellow:Statusgruppen::findAllByRangeId()} instead.',
+ 'getPersonsForRole' => 'Use #{yellow::Statusgruppen::members} instead.',
+ 'isVatherDaughterRelation' => false,
+ 'SetSelfAssign(' => false,
+ 'getExternDefaultForUser' => 'Use #{yellow:InstituteMember::getDefaultInstituteIdForUser($user_id)} instead.',
+ 'checkExternDefaultForUser' => 'Use #{yellow:InstituteMember::ensureDefaultInstituteIdForUser($user_id)} instead.',
+ 'getAllChildIDs' => false,
+ 'getKingsInformations' => 'Use #{yellow:User} model instead',
+
+ 'AutoInsert::existSeminars' => false,
+ 'new ZebraTable' => 'No longer neccessary. Use #{table.default} instead.',
+ 'new Table' => 'No longer neccessary. Use #{table.default} instead.',
+
+ //old datei.inc.php and visual.inc.php functions:
+ 'createSelectedZip' => 'Removed. Use #{yellow:FileArchiveManager::createArchiveFromFileRefs} instead.',
+ 'create_zip_from_directory' => 'Removed(?). Use #{yellow:FileArchiveManager::createArchiveFromPhysicalFolder} instead.',
+ 'getFileExtension' => 'Removed. Use PHP\'s built-in #{yellow:pathinfo($filename, PATHINFO_EXTENSION)} instead.',
+ 'get_icon_for_mimetype' => 'Removed. Use #{yellow:FileManager::getIconNameForMimeType} instead.',
+ 'get_upload_file_path' => 'Removed. Use #{yellow:File->getPath()} instead.',
+ 'GetDownloadLink' => 'Removed. Use one of the following alternatives instead: #{yellow:FileRef->getDownloadURL()}, #{yellow:FileManager::getDownloadLinkForArchivedCourse}, #{yellow:FileManager::getDownloadLinkForTemporaryFile} or #{yellow:FileManager::getDownloadURLForTemporaryFile}',
+ 'prepareFilename' => 'Removed. Use #{yellow:FileManager::cleanFileName} instead.',
+ 'GetFileIcon' => 'Removed. Use #{yellow:FileManager::getIconNameForMimeType} instead.',
+ 'parse_link' => 'Removed. Use #{yellow:FileManager::fetchURLMetadata} instead.',
+ 'unzip_file' => 'Removed. Use #{yellow:Studip\ZipArchive::extractToPath} or #yellow:Studip\ZipArchive::test} instead.',
+ 'datei.inc.php' => 'Removed. Use methods in functions.inc.php, FileManager, FileArchiveManager, FileRef, File or FolderType instead.',
+ 'TrackAccess' => 'Removed(?). Use {yellow:FileRef::incrementDownloadCounter}',
+ //StudipDocument and related classes:
+ 'StudipDocument(' => 'Removed(?). Use class #{yellow:FileRef} instead.',
+ 'DocumentFolder(' => 'Removed(?). Use class #{yellow:Folder} instead.',
+ 'StudipDocumentTree(' => 'Removed(?). Use class #{yellow:Folder} or #{yellow:FolderType} instead.',
+ 'WysiwygDocument' => 'Deprecated/To be removed. Use class #{yellow:FileRef} in conjunction with a #{yellow:FolderType} implementation instead.',
+
+ 'ZIP_USE_INTERNAL' => 'Removed. Please avoid querying the value of this configuration variable!',
+ 'ZIP_PATH' => 'Removed. Please avoid querying the value of this configuration variable!',
+ 'ZIP_OPTIONS' => 'Removed. Please avoid querying the value of this configuration variable!',
+ 'UNZIP_PATH' => 'Removed. Please avoid querying the value of this configuration variable!',
+
+ 'RuleAdministrationModel::getAdmissionRuleTypes' => 'Use #{yellow:AdmissionRule::getAvailableAdmissionRules(false)} instead.',
+ 'SessSemName' => 'Use class #{yellow:Context} instead',
+ '_SESSION["SessionSeminar"]' => 'Use class #{yellow:Context} instead',
+ '_SESSION[\'SessionSeminar\']' => 'Use class #{yellow:Context} instead',
+
+ 'Statusgruppe(' => 'Removed(?). Use class #{yellow:Statusgruppen} instead.',
+];
diff --git a/cli/compatibility-rules/studip-4.2.php b/cli/compatibility-rules/studip-4.2.php
new file mode 100644
index 0000000..4420e9f
--- /dev/null
+++ b/cli/compatibility-rules/studip-4.2.php
@@ -0,0 +1,17 @@
+<?php
+// "Rules"/definitions for critical changes in 4.2
+return [
+ 'get_perm' => 'Use the #{yellow:CourseMember} or #{yellow:InstitutMember} model instead.',
+ 'get_vorname' => 'Use #{yellow:User::find($id)->vorname} instead',
+ 'get_nachname' => 'Use #{yellow:User::find($id)->nachname} instead',
+ 'get_range_tree_path' => false,
+ 'get_seminar_dozent' => 'Use #{yellow:Course::find($id)->getMembersWithStatus(\'dozent\')} instead.',
+ 'get_seminar_tutor' => 'Use #{yellow:Course::find($id)->getMembersWithStatus(\'tutor\')} instead.',
+ 'get_seminar_sem_tree_entries' => false,
+ 'get_seminars_users' => 'Use #{yellow:CourseMember::findByUser($user_id)} instead to aquire all courses.',
+ 'remove_magic_quotes' => false,
+ 'text_excerpt' => false,
+ 'check_group_new' => false,
+ 'insertNewSemester' => 'Use the #{yellow:Semester} model instead.',
+ 'updateExistingSemester' => 'Use the #{yellow:Semester} model instead.',
+];
diff --git a/cli/compatibility-rules/studip-4.4.php b/cli/compatibility-rules/studip-4.4.php
new file mode 100644
index 0000000..48e5165
--- /dev/null
+++ b/cli/compatibility-rules/studip-4.4.php
@@ -0,0 +1,6 @@
+<?php
+// "Rules"/definitions for critical changes in 4.4
+return [
+ 'Token::is_valid' => 'Use #{yellow:Token::isValid($token, $user_id)} instead.',
+ 'Token::generate' => 'Use #{yellow:Token::create($duration = 30, $user_id = null)} instead.',
+];
diff --git a/cli/compatibility-rules/studip-5.0.php b/cli/compatibility-rules/studip-5.0.php
new file mode 100644
index 0000000..af8a70e
--- /dev/null
+++ b/cli/compatibility-rules/studip-5.0.php
@@ -0,0 +1,60 @@
+<?php
+// "Rules"/definitions for critical changes in 5.0
+return [
+ // https://develop.studip.de/trac/ticket/11250
+ 'userMayAccessRange' => '#{yellow:Changed} - Use #{yellow:isAccessibleToUser} instead',
+ 'userMayEditRange' => '#{yellow:Changed} - Use #{yellow:isEditableByUser} instead',
+ 'userMayAdministerRange' => '#{red:Removed}',
+
+ // UTF8-Encode/Decode legacy functions
+ 'studip_utf8encode' => '#{red:Removed} - Use utf8_encode().',
+ 'studip_utf8decode' => '#{red:Removed} - Use utf8_decode().',
+
+ // JSON encode/decode legacy functions
+ 'studip_json_decode' => '#{red:Deprecated} - Use json_decode() and pay attention to the second parameter.',
+ 'studip_json_encode' => '#{red:Deprecated} - Use json_encode().',
+
+ // https://develop.studip.de/trac/ticket/10806
+ 'SemesterData' => '#{red:Removed} - Use #{yellow:Semester model} instead',
+
+ // https://develop.studip.de/trac/ticket/10786
+ 'StatusgroupsModel' => '#{red:Removed} - Use #{yellow:Statusgruppen model} instead',
+
+ // https://develop.studip.de/trac/ticket/10796
+ 'StudipNullCache' => '#{red:Removed} - Use #{yellow:StudipMemoryCache} instead',
+
+ // https://develop.studip.de/trac/ticket/10838
+ 'getDeputies' => '#{red:Removed} - Use #{yellow:Deputy::findDeputies()} instead',
+ 'getDeputyBosses' => '#{red:Removed} - Use #{yellow:Deputy::findDeputyBosses()} instead',
+ '/(?<!Deputy::)addDeputy/' => '#{red:Removed} - Use #{yellow:Deputy::addDeputy()} instead',
+ '/deleteDeputy(?=\()/' => '#{red:Removed} - Use #{yellow:Deputy model} instead',
+ 'deleteAllDeputies' => '#{red:Removed} - Use #{yellow:Deputy::deleteByRange_id} instead',
+ '/(?<!Deputy::)isDeputy/' => '#{red:Removed} - Use #{yellow:Deputy::isDeputy()} instead',
+ 'setDeputyHomepageRights' => '#{red:Removed} - Use #{yellow:Deputy model} instead',
+ 'getValidDeputyPerms' => '#{red:Removed} - Use #{yellow:Deputy::getValidPerms()} instead',
+ 'isDefaultDeputyActivated' => '#{red:Removed} - Use #{yellow:Deputy::isActivated()} instead',
+ 'getMyDeputySeminarsQuery' => '#{red:Removed} - Use #{yellow:Deputy::getMySeminarsQuery()} instead',
+ 'isDeputyEditAboutActivated' => '#{red:Removed} - Use #{yellow:Deputy::isEditActivated()} instead',
+
+ // https://develop.studip.de/trac/ticket/10870
+ 'get_config' => '#{red:Deprecated} - Use #{yellow:Config::get()} instead.',
+
+ // https://develop.studip.de/trac/ticket/10919
+ 'RESTAPI\\RouteMap' => '#{red:Deprecated} - Use the #{yellow:JSONAPI} instead.',
+
+ // https://develop.studip.de/trac/ticket/10878
+ 'Leafo\\ScssPhp' => 'Library was replaced by #{yellow:scssphp/scssphp}',
+ 'sfYamlParser' => 'Library was replaced by #{yellow:symfony/yaml}',
+ 'DocBlock::of' => 'Library was replaced by #{yellow:gossi/docblock}',
+
+ 'vendor/idna_convert' => 'Remove include/require. Will be autoloaded.',
+ 'vendor/php-htmldiff' => 'Remove include/require. Will be autoloaded.',
+ 'vendor/HTMLPurifier' => 'Remove include/require. Will be autoloaded.',
+ 'vendor/phplot' => 'Remove include/require. Will be autoloaded.',
+ 'vendor/phpCAS' => 'Remove include/require. Will be autoloaded.',
+ 'vendor/phpxmlrpc' => 'Remove include/require. Will be autoloaded.',
+
+ // https://develop.studip.de/trac/ticket/10964
+ 'periodicalPushData' => '#{red:Removed} - Use #{yellow:STUDIP.JSUpdater.register()} instead',
+ '/UpdateInformtion::setInformation\(.+\..+\)/' => '#{red:Removed} - Use #{yellow:STUDIP.JSUpdater.register()} instead',
+];
diff --git a/cli/create_table_schemes.php b/cli/create_table_schemes.php
new file mode 100755
index 0000000..a7f557e
--- /dev/null
+++ b/cli/create_table_schemes.php
@@ -0,0 +1,47 @@
+#!/usr/bin/env php
+<?php
+# Lifter007: TODO
+# Lifter003: TODO
+/**
+* create_table_schemes.php
+*
+*
+*
+*
+* @author André Noack <noack@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+* @access public
+*/
+// +---------------------------------------------------------------------------+
+// This file is part of Stud.IP
+// create_table_schemes.php
+//
+// Copyright (C) 2006 André Noack <noack@data-quest.de>,
+// Suchi & Berg GmbH <info@data-quest.de>
+// +---------------------------------------------------------------------------+
+// 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 any later version.
+// +---------------------------------------------------------------------------+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+// +---------------------------------------------------------------------------+
+require_once dirname(__FILE__) . '/studip_cli_env.inc.php';
+exec("grep -l 'extends SimpleORMap' $STUDIP_BASE_PATH/lib/classes/*.class.php", $output, $ok);
+if(!$ok ){
+ fwrite(STDOUT, "<?php\n//copy to \$STUDIP_BASE_PATH/lib/dbviews/table_schemes.inc.php\n//generated ". date('r') ."\n");
+ foreach($output as $line){
+ require_once $line;
+ list($classname,,) = explode('.',basename($line));
+ $o = new $classname();
+ fwrite(STDOUT, $o->exportScheme());
+ }
+ fwrite(STDOUT, "?>");
+}
+
+?> \ No newline at end of file
diff --git a/cli/cronjob-worker.php b/cli/cronjob-worker.php
new file mode 100755
index 0000000..15f6e2e
--- /dev/null
+++ b/cli/cronjob-worker.php
@@ -0,0 +1,34 @@
+#!/usr/bin/env php
+<?php
+/**
+ * cronjob-worker - Worker process for the cronjobs
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @since 2.4
+ */
+
+// +---------------------------------------------------------------------------+
+// This file is part of Stud.IP
+// cronjob-worker.php
+//
+// Copyright (C) 2013 Jan-Hendrik Willms <tleilax+studip@gmail.com>
+// +---------------------------------------------------------------------------+
+// 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 any later version.
+// +---------------------------------------------------------------------------+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+// +---------------------------------------------------------------------------+
+
+ require_once 'studip_cli_env.inc.php';
+
+ CronjobScheduler::getInstance()->run(); \ No newline at end of file
diff --git a/cli/cronjobs.php b/cli/cronjobs.php
new file mode 100755
index 0000000..217c664
--- /dev/null
+++ b/cli/cronjobs.php
@@ -0,0 +1,54 @@
+#!/usr/bin/env php
+<?php
+/**
+* cronjobs - Helper script for the cronjobs
+*
+* @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+* @category Stud.IP
+* @since 3.1
+* @todo Parameter handling!
+*/
+
+require_once 'studip_cli_env.inc.php';
+
+$argc = $_SERVER['argc'];
+$argv = $_SERVER['argv'];
+
+$opts = getopt('hl', ['help', 'list']);
+
+if (isset($opts['l']) || isset($opts['list'])) {
+ $tasks = CronjobTask::findBySql('1');
+ foreach ($tasks as $task) {
+ $description = call_user_func([$task->class, 'getDescription']);
+ fwrite(STDOUT, sprintf('%s %s' . PHP_EOL, $task->id, $description));
+ }
+ exit(0);
+}
+
+if ($argc < 2 || isset($opts['h']) || isset($opts['help'])) {
+ fwrite(STDOUT,'Usage: ' . basename(__FILE__) . ' [--help] [--list] <task_id> [last_result]' . PHP_EOL);
+ exit(0);
+}
+
+
+$id = $_SERVER['argv'][1];
+$last_result = $argc > 2 ? $_SERVER['argv'][2] : null;
+$task = CronjobTask::find($id);
+if (!$task) {
+ fwrite(STDOUT, 'Unknown task id' . PHP_EOL);
+ exit(0);
+}
+
+if (!file_exists($GLOBALS['STUDIP_BASE_PATH'] . '/' . $task->filename)) {
+ fwrite(STDOUT, 'Invalid task, unknown filename "' . $task->filename . '"' . PHP_EOL);
+ exit(0);
+}
+
+require_once $task->filename;
+if (!class_exists($task->class)) {
+ fwrite(STDOUT, 'Invalid task, unknown class "' . $task->class . '"' . PHP_EOL);
+ exit(0);
+}
+
+$task->engage($last_result);
diff --git a/cli/describe_models.php b/cli/describe_models.php
new file mode 100755
index 0000000..ebe0db3
--- /dev/null
+++ b/cli/describe_models.php
@@ -0,0 +1,75 @@
+#!/usr/bin/env php
+<?php
+require_once 'studip_cli_env.inc.php';
+
+$dir = new FilesystemIterator($STUDIP_BASE_PATH . '/lib/models');
+foreach ($dir as $fileinfo) {
+ $class = mb_strstr($fileinfo->getFilename(), '.', true);
+ if (!in_array($class, words('SimpleCollection SimpleORMap SimpleORMapCollection StudipArrayObject')) && class_exists($class)) {
+ echo $class . "\n";
+ $model = new $class;
+ $meta = $model->getTableMetaData();
+ $props = [];
+ foreach ($meta['fields'] as $field => $info) {
+ $name = mb_strtolower($field);
+ $props[$name] = '@property string ' . $name;
+ $props[$name] .= ' database column';
+ if ($alias = array_search($name, $meta['alias_fields'])) {
+ $props[$alias] = '@property string ' . $alias;
+ $props[$alias] .= ' alias column for ' . $name;
+ }
+ }
+ foreach ($meta['additional_fields'] as $field => $info) {
+ $name = mb_strtolower($field);
+ $props[$name] = '@property string ' . $name;
+ $props[$name] .= ' computed column';
+ $getter = isset($info['get']) || method_exists($model, 'get' . $name);
+ $setter = isset($info['set']) || method_exists($model, 'set' . $name);
+
+ if ($setter && $getter) {
+ $props[$name] .= ' read/write';
+ } else if ($setter) {
+ $props[$name] .= ' read only';
+ }
+ }
+ foreach ($meta['relations'] as $relation) {
+ $options = $model->getRelationOptions($relation);
+ $props[$relation] = '@property ';
+ if ($options['type'] === 'has_many' ||
+ $options['type'] === 'has_and_belongs_to_many') {
+ $props[$relation] .= 'SimpleORMapCollection';
+ } else {
+ $props[$relation] .= $options['class_name'];
+ }
+ $props[$relation] .= ' ' . $relation;
+ $props[$relation] .= ' ' . $options['type'] . ' ' . $options['class_name'];
+ }
+ $props = array_map(function($p) {return ' * ' . $p . "\n";}, $props);
+ $file = file($fileinfo->getPathname());
+ foreach ($file as $n => $line) if (mb_strpos($line, 'class') === 0) break;
+ if ($n < count($file)) {
+ $classstart = $n;
+ $propend = null;
+ $propstart = null;
+ $docend = null;
+ for ($n; $n >= 0; --$n) {
+ if (!isset($docend) && mb_strpos($file[$n], ' */') === 0) $docend = $n;
+ if (!isset($propend) && mb_strpos($file[$n], ' * @property') === 0) $propend = $n;
+ if (isset($propend) && mb_strpos($file[$n], ' * @property') === 0) $propstart = $n;
+ }
+ if (isset($docend)) {
+ if (isset($propstart)) {
+ array_splice($file, $propstart, $propend-$propstart+1, $props);
+ } else {
+ array_splice($file, $docend, 0, $props);
+ }
+ $ok = file_put_contents($fileinfo->getPathname(), join('', array_map(function($l) {return rtrim($l, "\r\n") . PHP_EOL;}, $file)));
+ if ($ok) echo $fileinfo->getPathname() . " written \n";
+ else echo $fileinfo->getPathname() . " not writable \n";
+ } else {
+ echo 'no docblock found in ' . $fileinfo->getPathname() . chr(10);
+ }
+
+ }
+ }
+}
diff --git a/cli/dump_studip.php b/cli/dump_studip.php
new file mode 100755
index 0000000..52ce0c1
--- /dev/null
+++ b/cli/dump_studip.php
@@ -0,0 +1,87 @@
+#!/usr/bin/env php
+<?php
+/**
+* dump_studip.php
+*
+*
+*
+*
+* @author André Noack <noack@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+* @access public
+*/
+// +---------------------------------------------------------------------------+
+// This file is part of Stud.IP
+// dump_studip.php
+//
+// Copyright (C) 2011 André Noack <noack@data-quest.de>
+// +---------------------------------------------------------------------------+
+// 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 any later version.
+// +---------------------------------------------------------------------------+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+// +---------------------------------------------------------------------------+
+require_once 'studip_cli_env.inc.php';
+
+function exec_or_die($cmd) {
+ exec($cmd . ' 2>&1',$output,$ok);
+ if ($ok > 0) {
+ fwrite(STDOUT,join("\n", array_merge([$cmd], $output)) . "\n");
+ exit(1);
+ }
+}
+
+$dump_dir = $_SERVER['argv'][1] ? realpath($_SERVER['argv'][1]) : null;
+$dump_only = $_SERVER['argv'][2];
+
+if (!$dump_dir) {
+ fwrite(STDOUT,'Usage: ' . basename(__FILE__) . ' PATH [db|base|data]' .chr(10).'Dump all without second parameter.'.chr(10));
+ exit(0);
+}
+if (!is_writeable($dump_dir)) {
+ trigger_error('Directory: ' . $dump_dir . ' is not writeable!', E_USER_ERROR);
+}
+
+$today = date("Ymd");
+$prefix = Config::get()->STUDIP_INSTALLATION_ID ? Config::get()->STUDIP_INSTALLATION_ID : 'studip';
+if (!$dump_only || $dump_only == 'db') {
+ $dump_db_dir = $dump_dir . '/db-' . $today;
+ if (!is_dir($dump_db_dir)) {
+ mkdir($dump_db_dir);
+ }
+ foreach(DBManager::get()->query("SHOW TABLES") as $tables) {
+ $table = $tables[0];
+ $dump_table = $dump_db_dir . '/' . $table . '-' . $today . '.sql';
+ fwrite(STDOUT, 'Dumping database table ' . $table . chr(10));
+ exec_or_die("mysqldump -u$DB_STUDIP_USER -h$DB_STUDIP_HOST -p$DB_STUDIP_PASSWORD $DB_STUDIP_DATABASE $table > $dump_table");
+ }
+ $dump_db = $dump_dir . '/' . $prefix . '-DB-' . $today . '.tar.gz';
+ fwrite(STDOUT, 'Packing database to ' . $dump_db . chr(10));
+ exec_or_die("cd $dump_db_dir && tar -czf $dump_db *");
+ exec_or_die("rm -rf $dump_db_dir");
+}
+if (!$dump_only || $dump_only == 'base') {
+ $dumb_studip = $dump_dir . '/' . $prefix . '-BASE-' . $today . '.tar.gz';
+ $base_path = realpath($STUDIP_BASE_PATH);
+ if (!$base_path) {
+ trigger_error('Stud.IP directory not found!', E_USER_ERROR);
+ }
+ fwrite(STDOUT, 'Dumping Stud.IP directory to ' . $dumb_studip . chr(10));
+ exec_or_die("cd $base_path && tar -czf $dumb_studip --exclude 'data/*' .");
+}
+if (!$dump_only || $dump_only == 'data') {
+ $data_path = realpath($UPLOAD_PATH . '/../');
+ if ($data_path) {
+ $dumb_data = $dump_dir . '/' . $prefix . '-DATA-' . $today . '.tar.gz';
+ fwrite(STDOUT, 'Dumping data directory to ' . $dumb_data . chr(10));
+ exec_or_die("cd $data_path && tar -czf $dumb_data .");
+ }
+}
+exit(0);
diff --git a/cli/extract-js-localizations.php b/cli/extract-js-localizations.php
new file mode 100755
index 0000000..70d14a9
--- /dev/null
+++ b/cli/extract-js-localizations.php
@@ -0,0 +1,204 @@
+#!/usr/bin/env php
+<?php
+/**
+ * extract-js-localizations.php
+ *
+ * Exports all strings from js into app/views/localizations/show.php so
+ * they can be translated as well.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @copyright Stud.IP Core Group
+ * @since 3.1
+ */
+
+require 'studip_cli_env.inc.php';
+
+/**
+ * Determines whether the file should be skipped depending on an exclude list
+ * with an additional include list. This allows inclusion inside of previously
+ * excluded entries. We need this for the assets directory.
+ * Furthermore, the file is checked against a list of mime types to include.
+ *
+ * @param String $filename Adjusted filename (stripped to path inside trunk)
+ * @param String $realfile Actual file name (needed for mime type detection)
+ * @return bool indicating whether the file should be skipped or not
+ */
+function should_skip_file($filename, $realfile) {
+ $exclude = [
+ 'cli/*',
+ 'composer/*',
+ 'config/*',
+ 'data/*',
+ 'db/*',
+ 'doc/*',
+ 'locale/*',
+ 'node_modules/*',
+ 'public/assets/flash*',
+ 'public/assets/fonts*',
+ 'public/assets/images*',
+ 'public/assets/javascripts/*',
+ 'public/assets/sounds*',
+ 'public/assets/squeezed*',
+ 'public/assets/stylesheets*',
+ 'public/pictures/*',
+ 'public/plugins_packages/*',
+ 'test/*',
+ 'tests/*',
+ 'vendor/*',
+ ];
+ $include = [
+ 'public/assets/javascripts/ckeditor*',
+ 'public/plugins_packages/core*',
+ ];
+ $mime_types = [
+ 'text/*',
+ 'application/javascript',
+ ];
+
+ // Check if the file should be excluded, depending on it's path.
+ $matching_pattern = null;
+ $skip = false;
+ foreach ($exclude as $pattern) {
+ if (fnmatch($pattern, $filename)) {
+ $matching_pattern = $pattern;
+ $skip = true;
+ break;
+ }
+ }
+
+ // If it should be skipped in step 1, check if it matches the include
+ // patterns and no longer skip it, if it matches.
+ // Matches are only from patterns that are longer than the pattern that
+ // set the entry to be skipped. Thus it is detected if the file is in a
+ // subdirectory.
+ if ($skip) {
+ foreach ($include as $pattern) {
+ if (fnmatch($pattern, $filename) && mb_strlen($pattern) > mb_strlen($matching_pattern)) {
+ $skip = false;
+ break;
+ }
+ }
+ }
+
+ // If the file should not be skipped, check it's mime type and skip it
+ // if the mime type is not allowed.
+ if (!$skip && is_file($realfile)) {
+ $mime_type = mime_content_type($realfile);
+
+ $skip = true;
+ foreach ($mime_types as $pattern) {
+ if (fnmatch($pattern, $mime_type)) {
+ $skip = false;
+ break;
+ }
+ }
+ }
+
+ return $skip;
+}
+
+/**
+ * Extract the actual text strings from a file. This will only detect single
+ * line text strings. Multi line strings are just a hassle to handle in js
+ * anyways.
+ *
+ * @param String $file Filename to extract text strings from
+ * @return mixed Array with found text strings or false if no text strings
+ * were found
+ */
+function extract_strings($file) {
+ $contents = file_get_contents($file);
+ $regexp = '/(?:\'([^\']+)\'|"([^"]+)")\\.toLocaleString\\(\\s*\\)/';
+
+ if (preg_match_all($regexp, $contents, $matches, PREG_SET_ORDER)) {
+ $result = [];
+ foreach ($matches as $match) {
+ $result[] = $match[1] ?: $match[2];
+ }
+ return array_unique($result);
+ }
+
+ return false;
+}
+
+/**
+ * Recursively find text strings in files in the given directory.
+ * This skips invalid files.
+ *
+ * @param String $directory Directory to search files in
+ * @param mixed $base Optional base directory to strip from file names,
+ * will default to the initial passed directory.
+ * @return Array Associative array with filenames as index and an array of
+ * the text strings the file contains.
+ */
+function find_strings_in_dir($directory, $base = null) {
+ $result = [];
+
+ $base = rtrim($base ?: $directory, '/') . '/';
+
+ $files = glob(rtrim($directory, '/') . '/*');
+ foreach ($files as $file) {
+ $filename = str_replace($base, '', $file);
+ $is_dir = is_dir($file);
+
+ if (should_skip_file($filename, $file)) {
+ continue;
+ }
+
+ if (is_dir($file)) {
+ $result += find_strings_in_dir($file, $base);
+ } elseif ($strings = extract_strings($file)) {
+ $result[$filename] = $strings;
+ }
+ }
+
+ return $result;
+}
+
+// Find text strings in all stud.ip files
+$occurences = find_strings_in_dir(realpath(__DIR__ . '/..'));
+
+// Remove duplicates
+$hashes = [];
+foreach ($occurences as $file => $strings) {
+ foreach ($strings as $index => $string) {
+ $hash = md5($string);
+ if (in_array($hash, $hashes)) {
+ unset($strings[$index]);
+ } else {
+ $hashes[] = $hash;
+ }
+ }
+ if (empty($strings)) {
+ unset($occurences[$file]);
+ } else {
+ $occurences[$file] = $strings;
+ }
+}
+
+// Create trails view as output
+ob_start();
+?>
+<?= '<?php' . PHP_EOL ?>
+
+$translations = array(
+<? foreach ($occurences as $file => $strings): ?>
+ // <?= $file . PHP_EOL ?>
+<? foreach ($strings as $string): ?>
+ '<?= addcslashes($string, "'") ?>' => _('<?= addcslashes($string, "'") ?>'),
+<? endforeach; ?>
+
+<? endforeach; ?>
+);
+
+?>
+<?= '<?=' ?> json_encode($translations) <?= '?>' ?>
+<?
+$view = ob_get_clean();
+
+// Write output to the corresponding file
+file_put_contents(__DIR__ . '/../app/views/localizations/show.php', $view);
+
+// Show some statistics
+printf('%u strings written to file' . PHP_EOL, array_sum(array_map('count', $occurences)));
diff --git a/cli/fix_collate.php b/cli/fix_collate.php
new file mode 100755
index 0000000..b9b9f67
--- /dev/null
+++ b/cli/fix_collate.php
@@ -0,0 +1,24 @@
+#!/usr/bin/env php
+<?php
+/**
+ * @author Witali Mik <mik@data-quest.de>
+ * Script um Collation Konflikte automatisiert zu lösen
+ */
+
+require_once dirname(__FILE__) . '/studip_cli_env.inc.php';
+require_once 'lib/classes/DBManager.class.php';
+require_once 'config/config_local.inc.php';
+
+$charset = 'latin1';
+$collate = 'latin1_german1_ci';
+$sql = "SELECT CONCAT('ALTER TABLE `".$DB_STUDIP_DATABASE."`.`', TABLE_NAME, '` CONVERT TO CHARACTER SET ".$charset." COLLATE ".$collate.";') as query FROM `information_schema`.TABLES WHERE TABLE_SCHEMA='".$DB_STUDIP_DATABASE."' AND TABLE_COLLATION!='".$collate."'";
+
+$db = DBManager::get();
+
+
+$result = $db->query($sql);
+foreach($result->fetchAll(PDO::FETCH_OBJ) as $row){
+ $db->exec($row->query);
+ fwrite(STDOUT, sprintf("Execute: %s \n",$row->query));
+}
+fwrite(STDOUT, "Finished"); \ No newline at end of file
diff --git a/cli/fix_endtime_weekly_recurred_events.php b/cli/fix_endtime_weekly_recurred_events.php
new file mode 100755
index 0000000..7e31f29
--- /dev/null
+++ b/cli/fix_endtime_weekly_recurred_events.php
@@ -0,0 +1,33 @@
+#!/usr/bin/env php
+<?php
+/**
+ * fix_endtime_weekly_recurred_events.php
+ *
+ * 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 __DIR__ . '/studip_cli_env.inc.php';
+
+$events = EventData::findBySQL("rtype = 'WEEKLY' AND IFNULL(count, 0) > 0");
+$cal_event = new CalendarEvent();
+$i = 0;
+foreach ($events as $event) {
+ $id = $event->getId();
+ $cal_event->event = $event;
+ $rrule = $cal_event->getRecurrence();
+ $cal_event->setRecurrence($rrule);
+ $event->expire = $cal_event->event->expire;
+ $event->setId($id);
+ $event->store();
+ $i++;
+}
+
+fwrite(STDOUT, 'Wrong end time of recurrence fixed for ' . $i . ' events.' . chr(10));
+exit(1);
diff --git a/cli/getopts.php b/cli/getopts.php
new file mode 100644
index 0000000..dde439f
--- /dev/null
+++ b/cli/getopts.php
@@ -0,0 +1,317 @@
+<?php
+
+ /*
+
+ getopts by ALeX Kazik
+
+ Code: https://github.com/alexkazik/getopts
+ Docs: https://github.com/alexkazik/getopts/wiki/Documentation
+ Homepage: http://alex.kazik.de/195/getopts/
+
+ License: Creative Commons Attribution 3.0 Unported License
+ http://creativecommons.org/licenses/by/3.0/
+
+ */
+
+ function getopts($params, $args=NULL, $raw=false){
+ // check input
+ if(!is_array($params)){
+ trigger_error('Invalid params table', E_USER_ERROR);
+ }
+ if($args === NULL && is_array($_SERVER['argv'])){
+ $args = $_SERVER['argv'];
+ array_shift($args);
+ }
+ if(!is_array($args)){
+ trigger_error('Invalid args table', E_USER_ERROR);
+ }
+ if(!is_bool($raw)){
+ trigger_error('Invalid raw option', E_USER_ERROR);
+ }
+
+ // mb_substr, which returns '' in case of an empty mb_substr (usually false)
+ $mb_substr = function ($string, $start, $length = null) { // is not used, only for definition
+ $ret = call_user_func_array('mb_substr', func_get_args());
+ if ($ret === false){
+ return '';
+ } else {
+ return $ret;
+ }
+ };
+
+ // get arg (either implicit or the following)
+ $get_arg = function (&$next, &$args, &$num) { // pass by reference: num may be changed, others: performance
+ if ($next !== true) {
+ return $next;
+ } elseif ($num + 1 >= count($args)){
+ return false;
+ } else {
+ $num += 1;
+ return $args[$num];
+ }
+ };
+
+ // all types & subtypes
+ $types_subtypes = ['S' => 'stcr', 'V' => 'smar', 'O' => 'smar', 'A' => 'sr'];
+
+ // output
+ $Ores = [];
+ $Oerr = [];
+ $Oags = [];
+
+ // parsed options
+ $short = [];
+ $long = [];
+ $type = [];
+
+ // parse options
+ foreach($params AS $opt => $names){
+ if(is_string($names)){
+ $names = preg_split('/ +/', $names);
+ }
+ if(!is_array($names) || count($names) < 2){
+ trigger_error('Invalid type/name(s) to param "'.$opt.'"', E_USER_ERROR);
+ }
+
+ $ty = array_shift($names);
+ if(!is_string($ty) || mb_strlen($ty) < 1 || mb_strlen($ty) > 2){
+ trigger_error('Invalid type to param "'.$opt.'"', E_USER_ERROR);
+ }
+ $ty0 = $ty[0];
+ if(!isset($types_subtypes[$ty0])){
+ trigger_error('Invalid type to param "'.$opt.'"', E_USER_ERROR);
+ }
+ if(mb_strlen($ty) == 1){
+ $ty1 = $types_subtypes[$ty0][0];
+ }else{
+ $ty1 = $ty[1];
+ if(mb_strpos($types_subtypes[$ty0], $ty1) === false){
+ trigger_error('Invalid type to param "'.$opt.'"', E_USER_ERROR);
+ }
+ }
+ $type[$opt] = $ty0.$ty1;
+
+ foreach($names AS $name){
+ if(!is_string($name)){
+ trigger_error('Invalid names to param "'.$opt.'"', E_USER_ERROR);
+ }
+ if(!preg_match('!^(-)?([0-9a-zA-Z]+)$!', $name, $r)){
+ trigger_error('Invalid name to param "'.$opt.'"', E_USER_ERROR);
+ }
+ if($r[1] == '-' || mb_strlen($r[2]) > 1){
+ if(isset($long[$r[2]])){
+ trigger_error('Duplicate option name "'.$r[2].'"', E_USER_ERROR);
+ }
+ $long[$r[2]] = $opt;
+ }else{
+ if(isset($short[$r[2]])){
+ trigger_error('Duplicate option name "'.$r[2].'"', E_USER_ERROR);
+ }
+ $short[$r[2]] = $opt;
+ }
+ }
+
+ $Ores[$opt] = [];
+ }
+
+ // parse arguments
+ for($num=0; $num<count($args); $num++){
+ $arg = $args[$num];
+
+ if($arg == '--'){
+ // end of options, copy all other args
+ $num++;
+ for(; $num<count($args); $num++){
+ $Oags[] = $args[$num];
+ }
+ break;
+ }else if($arg == ''){
+ // empty -> skip
+ continue;
+ }else if($arg[0] != '-'){
+ // not an option -> copy to args
+ $Oags[] = $arg;
+ continue;
+ }
+
+ // this arg is an option!
+ if($arg[1] == '-'){
+ // long option
+ $p = mb_strpos($arg, '=');
+ if($p !== false){
+ $next = $mb_substr($arg, $p+1);
+ $arg = mb_substr($arg, 2, $p-2);
+ }else{
+ $next = true;
+ $arg = mb_substr($arg, 2);
+ }
+ if(!isset($long[$arg])){
+ $Oerr[] = 'Unknown option "--'.$arg.'"';
+ }else{
+ $opt = $long[$arg];
+ $Earg = '--'.$arg;
+ switch($type[$opt][0]){
+ case 'S':
+ $Ores[$opt][] = $next;
+ break;
+ case 'V':
+ if(($val = $get_arg($next,$args,$num)) === false){
+ $Oerr[] = 'Missing artument to option "'.$Earg.'"';
+ }else{
+ $Ores[$opt][] = $val;
+ }
+ break;
+ case 'O':
+ $Ores[$opt][] = $next;
+ break;
+ case 'A':
+ if(($val = $get_arg($next,$args,$num)) === false){
+ $Oerr[] = 'Missing artument to option "'.$Earg.'"';
+ }else{
+ $p = mb_strpos($val, '=');
+ if($p === false){
+ $Oerr[] = 'Malformed artument to option "'.$Earg.'" (a "=" is missing)';
+ }else if(isset($Ores[$opt][mb_substr($val, 0, $p)])){
+ $Oerr[] = 'Duplicate key "'.mb_substr($val, 0, $p).'" to option "'.$Earg.'"';
+ }else{
+ $Ores[$opt][mb_substr($val, 0, $p)] = $mb_substr($val, $p+1);
+ }
+ }
+ break;
+ }
+ }
+ }else{
+ // short option(s)
+ for($i=1; $i<mb_strlen($arg); $i++){
+ $c = $arg[$i];
+ $next = $mb_substr($arg, $i+1);
+ if($next == ''){
+ $next = true;
+ }else if($next[0] == '='){
+ $next = $mb_substr($next, 1);
+ }
+ if(!isset($short[$c])){
+ $Oerr[] = 'Unknown option "-'.$c.'"';
+ $i = mb_strlen($arg);
+ }else{
+ $opt = $short[$c];
+ $Earg = '-'.$c;
+ switch($type[$opt][0]){
+ case 'S':
+ $Ores[$opt][] = true;
+ break;
+ case 'V':
+ if(($val = $get_arg($next,$args,$num)) === false){
+ $Oerr[] = 'Missing artument to option "'.$Earg.'"';
+ }else{
+ $Ores[$opt][] = $val;
+ }
+ $i = mb_strlen($arg);
+ break;
+ case 'O':
+ $Ores[$opt][] = $next;
+ $i = mb_strlen($arg);
+ break;
+ case 'A':
+ if(($val = $get_arg($next,$args,$num)) === false){
+ $Oerr[] = 'Missing artument to option "'.$Earg.'"';
+ }else{
+ $p = mb_strpos($val, '=');
+ if($p === false){
+ $Oerr[] = 'Malformed artument to option "'.$Earg.'" (a "=" is missing)';
+ }else if(isset($Ores[$opt][mb_substr($val, 0, $p)])){
+ $Oerr[] = 'Duplicate key "'.mb_substr($val, 0, $p).'" to option "'.$Earg.'"';
+ }else{
+ $Ores[$opt][mb_substr($val, 0, $p)] = $mb_substr($val, $p+1);
+ }
+ }
+ $i = mb_strlen($arg);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // reformat result
+ if(!$raw){
+ foreach($Ores AS $opt => &$r){
+ switch($type[$opt]){
+ case 'Ss':
+ $r = count($r) > 0;
+ break;
+ case 'St':
+ $r = (count($r) & 1) == 1;
+ break;
+ case 'Sc':
+ $r = count($r);
+ break;
+
+ case 'Vs':
+ if(count($r) == 0){
+ // no option
+ $r = false;
+ }else{
+ // pick last entry
+ $r = array_pop($r);
+ }
+ break;
+
+ case 'Os':
+ if(count($r) == 0){
+ // no option
+ $r = false;
+ }else{
+ // pick last entry; if possible last used (non true) entry
+ do{
+ $rr = array_pop($r);
+ }while($rr === true && count($r) > 0);
+ $r = $rr;
+ }
+ break;
+
+ case 'Vm':
+ case 'Om':
+ if(count($r) == 0){
+ // no option
+ $r = false;
+ }else{
+ // as array
+ // (already done)
+ }
+ break;
+
+ case 'Va':
+ case 'Oa':
+ // false if none, direct (string) if only one, array otherwise
+ if(count($r) == 0){
+ // no option
+ $r = false;
+ }else if(count($r) == 1){
+ // a single option
+ $r = array_pop($r);
+ }else{
+ // as array
+ // (already done)
+ }
+ break;
+
+ case 'As':
+ // as array
+ // (already done)
+ break;
+
+ }
+ }
+ }
+
+ // errors?
+ if(count($Oerr) == 0){
+ $Oerr = false;
+ }
+
+ // result
+ return [$Oerr, $Ores, $Oags];
+ }
+
+?>
diff --git a/cli/help-translation-tool.php b/cli/help-translation-tool.php
new file mode 100755
index 0000000..6323eae
--- /dev/null
+++ b/cli/help-translation-tool.php
@@ -0,0 +1,543 @@
+#!/usr/bin/env php
+<?php
+/**
+ * help-translation-tool.php
+ *
+ * Exports db data for the help content, tooltips and tours into a .po file or
+ * reimports the translated strings into the db.
+ *
+ * Since we need to obtain the row to inssert/update the translated content,
+ * this information is coded into the corresponding filename and line number.
+ *
+ * By using a specific range for line number, we can determine what type the
+ * translated string is:
+ *
+ * range | context | location | file | line number
+ * --------------+------------+------------------------+-------+-------------
+ * 10000 - 19999 | - | help_content.label | route | position
+ * 20000 - 29999 | content_id | help_content.content | route | position
+ * 30000 - 39999 | tour_id | help_tours.name | - | version
+ * 40000 - 49999 | tour_id | help_tours.description | - | version
+ * 50000 - 59999 | tour_id | help_tour_steps.title | route | step
+ * 60000 - 69999 | tour_id | help_tour_steps.tip | route | step
+ * 70000 - 79999 | tooltip_id | help_tooltips.content | route | version
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @copyright Stud.IP Core Group
+ * @since 3.1
+ */
+
+require_once 'studip_cli_env.inc.php';
+
+define('MAX_LINE_LENGTH', 60);
+
+/**
+ * Escapes a string for use in .po file.
+ *
+ * @param String $string String to escape
+ * @return String Escaped string
+ */
+function po_escape($string) {
+ return str_replace('"', '\\"', $string);
+}
+
+/**
+ * Unescapes a string for use in .po file.
+ *
+ * @param String $string String to unescape
+ * @return String Unescaped string
+ */
+function po_unescape($string) {
+ $replaces = [
+ '\\"' => '"',
+ '\\n' => "\n",
+ ];
+ $string = str_replace(array_keys($replaces), array_values($replaces), $string);
+ return $string;
+}
+
+/**
+ * Prepares a string for use in .po file.
+ *
+ * @param String $string String to use in .po file
+ * @return String Processed string
+ */
+function po_stringify($string) {
+ $string = str_replace("\r", '', $string);
+ $chunks = explode("\n", $string);
+
+ if (count($chunks) === 1 && mb_strlen($chunks[0]) < MAX_LINE_LENGTH) {
+ return '"' . po_escape($chunks[0]) . '"';
+ }
+
+ $result = '""' . "\n";
+ foreach ($chunks as $index => $chunk) {
+ $chunk = wordwrap($chunk, MAX_LINE_LENGTH);
+ $parts = explode("\n", $chunk);
+ foreach ($parts as $idx => $line) {
+ $current_last = $idx === count($parts) - 1;
+ $last = ($current_last && $index === count($chunks) - 1);
+
+ $result .= '"' . po_escape($line) . ($last ? '' : ($current_last ? '\\n' : ' ')) . '"'. "\n";
+ }
+ }
+ return rtrim($result, "\n");
+}
+
+/**
+ * Returns the id for a help entitiy based on the given index and other
+ * credentials. This function also copies existing data and settings if
+ * the entity in the given language is newly created.
+ *
+ * @param String $version Stud.IP version to use for the new entry
+ * @param String $language Language to use for the new entry
+ * @param Array $message Complete message item from parsed .po file
+ * @param String $route Associated route (if any)
+ * @param int $index Type index for the entity
+ * @param int $position Position/version of the entity
+ * @return String Id of the entity
+ */
+function get_id($version, $language, $message, $route, $index, $position) {
+ static $ids = [];
+
+ if ($index < 3) {
+ // Entity is help content
+ $hash = md5('content#' . join('#', compact(words('temp version language route position'))));
+ } elseif ($index < 7) {
+ // Entity is help tour content
+ $hash = md5('tour#' . $message['context'] . '#' . join('#' , compact(words('version language'))));
+ } elseif ($index == 7) {
+ // Entity is help tooltip
+ $hash = md5('tooltip#' . $message['context'] . '#' . join(words('position language')));
+ } else {
+ throw new RuntimeException('Unknown index "' . $index . '"');
+ }
+
+ // If id has not yet been generated
+ if (!isset($ids[$hash])) {
+ if ($index < 3) {
+ // Help content
+
+ // Try to get content id by primary key
+ $query = "SELECT content_id
+ FROM help_content
+ WHERE route = :route AND studip_version = :version
+ AND language = :language AND position = :position
+ AND custom = 0";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':route', $route);
+ $statement->bindValue(':version', $version);
+ $statement->bindValue(':language', $language);
+ $statement->bindValue(':position', $position);
+ $statement->execute();
+
+ // Use found id or generate new one
+ $id = $statement->fetchColumn() ?: md5(uniqid('help_content', true));
+ $ids[$hash] = $id;
+ } elseif ($index < 7) {
+ // Help tour
+
+ // Is there any previous generated content?
+ // We have to use the hash generated above as the new id since
+ // there is no other way to exactly identify an already created
+ // entity for the given language and version
+ $query = "SELECT tour_id
+ FROM help_tours
+ WHERE tour_id = :tour_id";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':tour_id', $hash);
+ $statement->execute();
+
+ $id = $statement->fetchColumn();
+ if (!$id) {
+ // If no previous generated content is available, prepare
+ // database for new content
+ $id = $hash;
+
+ // Copy settings from tour
+ $query = "INSERT INTO help_tours
+ SELECT :id AS tour_id, '' AS name, '' AS description,
+ type, roles, version, :language AS language,
+ :version AS studip_version, installation_id,
+ UNIX_TIMESTAMP() AS mkdate
+ FROM help_tours
+ WHERE tour_id = :tour_id";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':id', $id);
+ $statement->bindValue(':language', $language);
+ $statement->bindValue(':version', $version);
+ $statement->bindValue(':tour_id', $message['context']);
+ $statement->execute();
+
+ // Copy individual steps
+ $query = "INSERT INTO help_tour_steps
+ SELECT :id AS tour_id, step, '' AS title, '' AS tip,
+ orientation, interactive, css_selector, route,
+ author_id, UNIX_TIMESTAMP() AS mkdate
+ FROM help_tour_steps
+ WHERE tour_id = :tour_id";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':id', $id);
+ $statement->bindValue(':tour_id', $message['context']);
+ $statement->execute();
+
+ // Copy tour audiences
+ $query = "INSERT INTO help_tour_audiences
+ SELECT :id AS tour_id, range_id, type
+ FROM help_tour_audiences
+ WHERE tour_id = :tour_id";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':id', $id);
+ $statement->bindValue(':tour_id', $message['context']);
+ $statement->execute();
+
+ // Copy tour settings
+ $query = "INSERT INTO help_tour_settings
+ SELECT :id AS tour_id, active, access
+ FROM help_tour_settings
+ WHERE tour_id = :tour_id";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':id', $id);
+ $statement->bindValue(':tour_id', $message['context']);
+ $statement->execute();
+ }
+ $ids[$hash] = $id;
+ } elseif ($index == 7) {
+ // Help tooltip
+
+ // Nothing needs to be done, just copy the tooltip id
+ // (This is the only table that has the id and version/language
+ // info as primary key)
+ $ids[$hash] = $message['context'];
+ }
+ }
+
+ // Return id from cache
+ return $ids[$hash];
+}
+
+// Error message: Not via cli or invalid parameters
+if (!isset($_SERVER['argv'], $_SERVER['argc']) || $_SERVER['argc'] < 2) {
+ print 'Usage: ' . (@$_SERVER['argv'][0] ?: basename(__FILE__)) . ' [--version] [--language] [--force] <import|export> [file]' . "\n";
+ die(1);
+}
+
+// Parse command line options
+$opts = [
+ 'short' => 'v:l:f',
+ 'long' => [
+ 'force',
+ 'version:',
+ 'language:'
+ ]
+];
+$options = getopt($opts['short'], $opts['long']);
+$force = isset($options['f']) || isset($options['force']);
+$version = @$options['version'] ?: @$options['v']
+ ?: DBManager::get()->query("SELECT MAX(studip_version) FROM help_content LIMIT 1")->fetchColumn()
+ ?: $GLOBALS['SOFTWARE_VERSION'];
+$language = @$options['language'] ?: @$options['l'] ?: mb_substr(Config::get()->DEFAULT_LANGUAGE, 0, 2);
+
+// Remove option from arguments
+$remove = [];
+foreach (str_split($opts['short']) as $opt) {
+ if ($opt !== ':') {
+ $remove[] = '-' . $opt;
+ }
+}
+foreach ($opts['long'] as $opt) {
+ $remove[] = '--' . rtrim($opt, ':');
+}
+$_SERVER['argv'] = array_values(array_diff($_SERVER['argv'], $remove));
+
+if ($_SERVER['argv'][1] === 'export') {
+ // Export
+
+ // Get output file name
+ // Either from second parameter or use default at temp path
+ $output = $_SERVER['argv'][2] ?: ($GLOBALS['TMP_PATH'] . '/studip-help-content-' . $version . '-' . $language . '.po');
+
+ // Error message: Script will not overwrite existing file unless forced
+ if (file_exists($output) && !$force) {
+ printf('Error: Output file "%s" exists. Use --force to overwrite.' . "\n", $output);
+ die(2);
+ }
+
+ // Error message: Output directory does not exist
+ $output_dir = dirname($output);
+ if (!file_exists($output_dir)) {
+ printf('Error: Directory for output "%s" does not exist.' . "\n", $output_dir);
+ die(3);
+ }
+ // Error message: Output directory is not writable
+ if (!is_writable($output_dir)) {
+ printf('Error: Directory for output "%s" is not writable.' . "\n", $output_dir);
+ die(4);
+ }
+
+ // Open output file for writing
+ $fp = fopen($output, 'w+');
+ // Error message: Output file could not be openend for writing
+ if (!is_resource($fp)) {
+ printf('Error: Could not open output file "%s" for writing.' . "\n", $output);
+ die(5);
+ }
+
+ // Write .po header
+ fputs($fp, '# Jan-Hendrik Willms <tleilax+studip@gmail.com>, 2014.' . "\n");
+ fputs($fp, '# Generated content' . "\n");
+ fputs($fp, 'msgid ""' . "\n");
+ fputs($fp, 'msgstr ""' . "\n");
+ fputs($fp, '"Project-Id-Version: STUDIP-' . $GLOBALS['SOFTWARE_VERSION'] . '\\n"' . "\n");
+ fputs($fp, '"Language: STUDIP-' . $language . '\\n"' . "\n");
+ fputs($fp, '"Report-Msgid-Bugs-To: tleilax+studip@gmail.com' . '\\n"' . "\n");
+ fputs($fp, '"POT-Creation-Date: ' . date('r') . '\\n"' . "\n");
+ fputs($fp, '"PO-Revision-Date: ' . date('r') . '\\n"' . "\n");
+ fputs($fp, '"Last-Translator: Stud.IP Core Group <info@studip.de>\\n"' . "\n");
+ fputs($fp, '"Language-Team: Stud.IP Core Group <info@studip.de>\\n"' . "\n");
+ fputs($fp, '"MIME-Version: 1.0\\n"' . "\n");
+ fputs($fp, '"Content-Type: text/plain; charset=UTF-8\\n"' . "\n");
+ fputs($fp, '"Content-Transfer-Encoding: 8bit\\n"' . "\n");
+ fputs($fp, "\n");
+
+ // Load all data from db in one big query
+ $query = "SELECT label AS content, CONCAT(route, ':', 10000 + position) AS occurence
+ FROM help_content
+ WHERE studip_version = :version
+ AND language = :language
+ AND custom = 0
+ -- Help content label
+
+ UNION
+
+ SELECT CONCAT(content, '{#$#}', content_id) AS content, CONCAT(route, ':', 20000 + position) AS occurence
+ FROM help_content
+ WHERE studip_version = :version
+ AND language = :language
+ AND custom = 0
+ -- Actual help content
+
+ UNION
+
+ SELECT CONCAT(name, '{#$#}', tour_id) AS content, CONCAT('tours.php:', 30000 + version) AS occurence
+ FROM help_tours
+ WHERE studip_version = :version
+ AND language = :language
+ -- Help tour name
+
+ UNION
+
+ SELECT CONCAT(description, '{#$#}', tour_id) AS content, CONCAT('tours.php:', 40000 + version) AS occurence
+ FROM help_tours
+ WHERE studip_version = :version
+ AND language = :language
+ -- Help tour description
+
+ UNION
+
+ SELECT CONCAT(title, '{#$#}', tour_id) AS content, CONCAT(route, ':', 50000 + step) AS occurence
+ FROM help_tour_steps
+ JOIN help_tours USING (tour_id)
+ WHERE studip_version = :version
+ AND language = :language
+ -- Individual help tour step title
+
+ UNION
+
+ SELECT CONCAT(tip, '{#$#}', tour_id) AS content, CONCAT(route, ':', 60000 + step) AS occurence
+ FROM help_tour_steps
+ JOIN help_tours USING (tour_id)
+ WHERE studip_version = :version
+ AND language = :language
+ -- Individual help tour step content
+
+ UNION
+
+ SELECT CONCAT(t0.content, '{#$#}', t0.tooltip_id) AS content, CONCAT(t0.route, ':', 70000 + t0.version) AS occurence
+ FROM help_tooltips AS t0
+ LEFT JOIN help_tooltips AS t1
+ ON t0.language = t1.language
+ AND t0.tooltip_id = t1.tooltip_id
+ AND t0.version < t1.version
+ WHERE t0.language = :language AND t1.tooltip_id IS NULL
+ -- Help tooltip
+ ";
+ $statement = DBManager::get()->prepare($query);
+ $statement->bindValue(':version', $version);
+ $statement->bindValue(':language', $language);
+ $statement->execute();
+ $statement->setFetchMode(PDO::FETCH_GROUP | PDO::FETCH_COLUMN);
+
+ // Loop through each row and write .po entry
+ foreach ($statement as $content => $occurences) {
+ list($content, $context) = explode('{#$#}', $content);
+
+ fputs($fp, '#: ' . implode(' ', $occurences) . "\n");
+ if ($context) {
+ fputs($fp, 'msgctxt "' . $context . '"' . "\n");
+ }
+ fputs($fp, 'msgid ' . po_stringify($content) . "\n");
+ fputs($fp, 'msgstr ""' . "\n");
+ fputs($fp, "\n");
+ }
+
+ // Close output file
+ fclose($fp);
+} elseif ($_SERVER['argv'][1] === 'import') {
+ // Import
+
+ // Error message: Invalid parameters
+ if ($_SERVER['argc'] < 4) {
+ print 'Usage: ' . $_SERVER['argv'][0] . ' import [--language] <file> <version>';
+ die(6);
+ }
+
+ // Set input file and version from parameters
+ $input = $_SERVER['argv'][2];
+ $version = $_SERVER['argv'][3];
+
+ // Error message: Input file does not exists or is not readable
+ if (!file_exists($input) || !is_readable($input)) {
+ printf('Error: Input file "%s" does not exist or is not readable.' . "\n", $input);
+ die(7);
+ }
+
+ // Open input file for reading
+ $fp = fopen($input, 'r');
+ // Error message: Input file could not be opened for reading
+ if (!is_resource($fp)) {
+ printf('Error: Could not open input file "%s" for reading.' . "\n", $input);
+ die(5);
+ }
+
+ // Parse input .po file
+ // This is pretty straight forward, yet hacky.
+ // The script tries to detect comments (only #:, # by itself is ignored),
+ // message context, message id and message content in this order.
+ // Any empty line will write to messages array.
+ // This routine will probably break for any .po file that differs from the
+ // ones created in transifex.
+ // This is just supposed to work, not to be beautiful. ;)
+ $messages = [];
+ $context = '';
+ $id = '';
+ $content = '';
+ $occurences = [];
+ $last = false;
+ $count = 0;
+ while (!feof($fp) && $row = fgets($fp)) {
+ $count += 1;
+
+ $row = trim($row);
+ if ($row[0] === '#' && $row[1] !== ':') {
+ continue;
+ }
+ if ($row[0] === '#') {
+ $occurences = array_merge($occurences, explode(' ', mb_substr($row, 2)));
+ $occurences = array_filter($occurences);
+ $last = 'occurence';
+ } elseif (preg_match('/^\msgctxt\\s+"(.*?)"$/', $row, $match)) {
+ $context = $match[1];
+ $last = 'context';
+ } elseif (preg_match('/^msgid\\s+"(.*?)"$/', $row, $match)) {
+ $id = po_unescape($match[1]);
+ $last = 'id';
+ } elseif (preg_match('/^msgstr\\s+"(.*?)"$/', $row, $match)) {
+ $content = po_unescape($match[1]);
+ $last = 'content';
+ } elseif (preg_match('/^"(.*?)"$/', $row, $match) && in_array($last, words('id content'))) {
+ if ($last === 'id') {
+ $id .= po_unescape($match[1]);
+ } else {
+ $content .= po_unescape($match[1]);
+ }
+ } elseif (!$row && $last === 'content') {
+ $messages[$context . '#' . $id] = compact(words('context id content occurences'));
+
+ $context = '';
+ $id = '';
+ $content = '';
+ $occurences = [];
+ $last = false;
+ } else {
+ printf('Parse error at line %u.' . "\n", $count);
+ printf('Last item was "%s".' . "\n", $last);
+ printf('Current row: %s' . "\n", $row);
+ die(6);
+ }
+ }
+ fclose($fp);
+
+ // Parse meta information (no context & no id = item at '#')
+ $meta = [];
+ foreach (explode("\n", $messages['#']['content']) as $row) {
+ $row = trim($row);
+ if (!$row) {
+ continue;
+ }
+
+ list($index, $content) = array_map('trim', explode(':', $row, 2));
+ $meta[$index] = $content;
+ }
+ unset($messages['#']);
+
+ // Get language
+ $language = mb_strtolower($meta['Language']);
+
+ // Define db queries for each type (see comment block at the top of
+ // this file, type is distinguished by the line number / 10000)
+ $queries = [];
+ $queries[1] = "INSERT INTO help_content (content_id, language, label, icon, content, route, studip_version, position, custom, installation_id, mkdate)
+ VALUES (:id, :language, :content, 'info', '', :route, :version, :position, 0, '', UNIX_TIMESTAMP())
+ ON DUPLICATE KEY UPDATE label = VALUES(label)";
+ $queries[2] = "INSERT INTO help_content (content_id, language, label, icon, content, route, studip_version, position, custom, installation_id, mkdate)
+ VALUES (:id, :language, '', 'info', :content, :route, :version, :position, 0, '', UNIX_TIMESTAMP())
+ ON DUPLICATE KEY UPDATE content = VALUES(content)";
+ $queries[3] = "INSERT INTO help_tours (tour_id, name, description, type, roles, version, language, studip_version, installation_id, mkdate)
+ VALUES (:id, :content, '', 'tour', '', :position, :language, :version, '', UNIX_TIMESTAMP())
+ ON DUPLICATE KEY UPDATE name = VALUES(name)";
+ $queries[4] = "INSERT INTO help_tours (tour_id, name, description, type, roles, version, language, studip_version, installation_id, mkdate)
+ VALUES (:id, '', :content, 'tour', '', :position, :language, :version, '', UNIX_TIMESTAMP())
+ ON DUPLICATE KEY UPDATE description = VALUES(description)";
+ $queries[5] = "INSERT INTO help_tour_steps (tour_id, step, title, tip, interactive, css_selector, route, author_id, mkdate)
+ VALUES (:id, :position, :content, '', 0, '', :route, '', UNIX_TIMESTAMP())
+ ON DUPLICATE KEY UPDATE title = VALUES(title)";
+ $queries[6] = "INSERT INTO help_tour_steps (tour_id, step, title, tip, interactive, css_selector, route, author_id, mkdate)
+ VALUES (:id, :position, '', :content, 0, '', :route, '', UNIX_TIMESTAMP())
+ ON DUPLICATE KEY UPDATE tip = VALUES(tip)";
+ $queries[7] = "INSERT INTO help_tooltips (tooltip_id, language, version, content, author_id, mkdate, route)
+ VALUES (:id, :language, :position, :content, '', UNIX_TIMESTAMP(), :route)
+ ON DUPLICATE KEY UPDATE content = VALUES(content)";
+
+ // Prepare statements and prebind version and language
+ $statements = array_map([DBManager::get(), 'prepare'], $queries);
+ foreach ($statements as $index => $statement) {
+ $statement->bindValue(':version', $version);
+ $statement->bindValue(':language', $language);
+
+ $statements[$index] = $statement;
+ }
+
+ // Process each message, skip the ones with empty content
+ foreach ($messages as $message) {
+ if (empty($message['content'])) {
+ continue;
+ }
+
+ foreach ($message['occurences'] as $occurence) {
+ list($route, $lineno) = explode(':', $occurence);
+ $index = floor($lineno / 10000);
+ $position = $lineno % 10000;
+
+ $id = get_id($version, $language, $message, $route, $index, $position);
+
+ $statement = $statements[$index];
+ $statement->bindValue(':id', $id);
+ $statement->bindValue(':content', $message['content']);
+ $statement->bindValue(':route', $route);
+ $statement->bindValue(':position', $position);
+ $statement->execute();
+ }
+ }
+}
diff --git a/cli/i18n-plugin.php b/cli/i18n-plugin.php
new file mode 100755
index 0000000..e301413
--- /dev/null
+++ b/cli/i18n-plugin.php
@@ -0,0 +1,89 @@
+#!/usr/bin/env php
+<?php
+require_once 'studip_cli_env.inc.php';
+
+if ($_SERVER['argc'] < 3) {
+ fwrite(STDOUT, 'Stud.IP plugin localization tool - Tools for the localization of a plugin' . PHP_EOL);
+ fwrite(STDOUT, '=========================================================================' . PHP_EOL);
+ fwrite(STDOUT, 'Usage: ' . basename(__FILE__) . ' <folder> <command>' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ fwrite(STDOUT, '<folder> is the folder of the plugin you want to localize.' . PHP_EOL);
+ fwrite(STDOUT, '<command> is any of the commands listed below.' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ fwrite(STDOUT, 'Commands:' . PHP_EOL);
+ fwrite(STDOUT, ' detect - Detects probably unmarked strings for localization in php files.' . PHP_EOL);
+ fwrite(STDOUT, ' extract - Extracts the localizable string from php files into a .pot file.' . PHP_EOL);
+ fwrite(STDOUT, ' compile - Compiles all .po files in the locale folder of the plugin' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ exit(0);
+}
+
+$plugin_folder = $_SERVER['argv'][1];
+$command = $_SERVER['argv'][2];
+
+if (!is_dir($plugin_folder)) {
+ $plugin_folder = rtrim($GLOBALS['ABSOLUTE_PATH_STUDIP'], '/') . '/' . ltrim($plugin_folder, '/');
+}
+if (!is_dir($plugin_folder)) {
+ fwrite(STDERR, 'Error: ' . $_SERVER['argv'][2] . ' is not a valid folder' . PHP_EOL);
+ exit(0);
+}
+
+$plugin_folder = rtrim($plugin_folder, '/');
+
+if (!file_exists($plugin_folder. '/plugin.manifest')) {
+ fwrite(STDERR, 'Error: ' . $_SERVER['argv'][2] . ' is not a valid plugin folder. Manifest is missing.' . PHP_EOL);
+ exit(0);
+}
+$manifest = parse_ini_file($plugin_folder . '/plugin.manifest', false, INI_SCANNER_RAW);
+
+$languages = array_map(function ($lang) {
+ return explode('_', $lang)[0];
+}, array_keys($GLOBALS['INSTALLED_LANGUAGES']));
+
+if ($command === 'detect') {
+ $iterator = new RecursiveDirectoryIterator($plugin_folder, FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::UNIX_PATHS);
+ $iterator = new RecursiveIteratorIterator($iterator);
+ $regexp_iterator = new RegexIterator($iterator, '/\.php$/', RecursiveRegexIterator::MATCH);
+
+ foreach ($regexp_iterator as $file) {
+ $filename = $file->getPathName();
+ if (preg_match('/(?<![$>])_\(/', file_get_contents($filename))) {
+ fwrite(STDOUT, "{$filename}" . PHP_EOL);
+ }
+ }
+
+ // system("ack -l '(?<![$>])_\(' {$plugin_folder}");
+} elseif ($command === 'extract') {
+ if (!isset($manifest['localedomain'])) {
+ fwrite(STD_ERROR, 'No localedomain found in plugin manifest' . PHP_EOL);
+ }
+
+ $pot_name = $manifest['localedomain'];
+
+ foreach (array_keys($GLOBALS['CONTENT_LANGUAGES']) as $lang) {
+ $lang = explode('_', $lang)[0];
+ $language_dir = "{$plugin_folder}/locale/{$lang}/LC_MESSAGES";
+ if (!file_exists($language_dir)) {
+ mkdir($language_dir, 0755, true);
+ }
+ }
+
+ $main_lang = reset($languages);
+ $pot_file = "{$plugin_folder}/locale/{$main_lang}/LC_MESSAGES/{$pot_name}.pot";
+ file_put_contents($pot_file, '');
+
+ system("find {$plugin_folder} -iname '*.php' | xargs xgettext --keyword=_n:1,2 --from-code=UTF-8 -j -n --language=PHP --add-location=never --package-name={$manifest['pluginclassname']} -o {$pot_file}");
+} elseif ($command === 'compile') {
+ foreach (glob("{$plugin_folder}/locale/*/LC_MESSAGES/*.po") as $po) {
+ $mo = preg_replace('/\.po$/', '.mo', $po);
+ system("msgfmt {$po} -o {$mo}");
+ }
+
+} else {
+ fwrite(STDERR, 'Unknown command: ' . $_SERVER['argv'][1] . PHP_EOL);
+ exit(0);
+}
+
+exit(1);
+
diff --git a/cli/kill_studip_user.php b/cli/kill_studip_user.php
new file mode 100755
index 0000000..7798fa4
--- /dev/null
+++ b/cli/kill_studip_user.php
@@ -0,0 +1,95 @@
+#!/usr/bin/env php
+<?php
+# Lifter003: TEST
+# Lifter007: TODO
+/**
+* kill_studip_user.php
+*
+*
+*
+*
+* @author André Noack <noack@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+* @access public
+*/
+// +---------------------------------------------------------------------------+
+// This file is part of Stud.IP
+// kill_studip_user.php
+//
+// Copyright (C) 2006 André Noack <noack@data-quest.de>,
+// Suchi & Berg GmbH <info@data-quest.de>
+// +---------------------------------------------------------------------------+
+// 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 any later version.
+// +---------------------------------------------------------------------------+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+// +---------------------------------------------------------------------------+
+define('SEND_MAIL_ON_DELETE', 1);
+define('KILL_ADMINS' , 0);
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+
+if (SEND_MAIL_ON_DELETE && !($MAIL_LOCALHOST && $MAIL_HOST_NAME && $ABSOLUTE_URI_STUDIP)){
+ trigger_error('To use this script you MUST set correct values for $MAIL_LOCALHOST, $MAIL_HOST_NAME and $ABSOLUTE_URI_STUDIP in local.inc!', E_USER_ERROR);
+}
+
+$argc = $_SERVER['argc'];
+$argv = $_SERVER['argv'];
+
+if (!$argv[1]){
+ fwrite(STDOUT,'Usage: ' . basename(__FILE__) . ' [file][-] (use - to read from STDIN)' .chr(10));
+ exit(0);
+}
+if ($argv[1] == '-'){
+ $fo = STDIN;
+} elseif (is_file($argv[1])){
+ $fo = fopen($argv[1],'r');
+} else {
+ trigger_error("File not found: {$argv[1]}", E_USER_ERROR);
+}
+
+$list = '';
+while (!feof($fo)) {
+ $list .= fgets($fo, 1024);
+}
+
+$kill_list = preg_split("/[\s,;]+/", $list, -1, PREG_SPLIT_NO_EMPTY);
+$kill_list = array_unique($kill_list);
+
+$query = "SELECT * FROM auth_user_md5 WHERE username IN (?)";
+$statement = DBManager::get()->prepare($query);
+$statement->execute([$kill_list ?: '']);
+while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
+ $kill_user[$row['username']] = $row;
+}
+if (!is_array($kill_user)) {
+ fwrite(STDOUT, 'No user from list found in database.' . chr(10));
+ exit(0);
+}
+
+foreach($kill_user as $uname => $udetail){
+ if (!KILL_ADMINS && ($udetail['perms'] == 'admin' || $udetail['perms'] == 'root')){
+ fwrite(STDOUT, "user: $uname is '{$udetail['perms']}', NOT deleted". chr(10));
+ } else {
+ $umanager = new UserManagement($udetail['user_id']);
+ //wenn keine Email gewünscht, Adresse aus den Daten löschen
+ if (!SEND_MAIL_ON_DELETE) $umanager->user_data['auth_user_md5.Email'] = '';
+ if ($umanager->deleteUser()){
+ fwrite(STDOUT, "user: $uname successfully deleted:". chr(10)
+ . parse_msg_to_clean_text($umanager->msg)
+ . chr(10));
+ } else {
+ fwrite(STDOUT, "user: $uname NOT deleted:". chr(10)
+ . parse_msg_to_clean_text($umanager->msg)
+ . chr(10));
+ }
+ }
+}
+exit(1);
diff --git a/cli/migrate.php b/cli/migrate.php
new file mode 100755
index 0000000..2b43b17
--- /dev/null
+++ b/cli/migrate.php
@@ -0,0 +1,76 @@
+#!/usr/bin/env php
+<?php
+# Lifter007: TODO
+# Lifter003: TODO
+/*
+ * migrate.php - Migrations for Stud.IP
+ *
+ * Copyright (C) 2006 - Marcus Lunzenauer <mlunzena@uos.de>
+ *
+ * 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.
+ */
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+
+if (isset($_SERVER['argv'])) {
+ # check for command line options
+ $options = getopt('1:d:lm:t:v');
+ if ($options === false) {
+ exit(1);
+ }
+
+ # check for options
+ $single = false;
+ $domain = 'studip';
+ $list = false;
+ $path = $STUDIP_BASE_PATH . '/db/migrations';
+ $verbose = false;
+ $target = null;
+
+ foreach ($options as $option => $value) {
+ switch ($option) {
+ case '1':
+ $single = (string) $value;
+ break;
+ case 'd':
+ $domain = (string) $value;
+ break;
+ case 'l':
+ $list = true;
+ break;
+ case 'm':
+ $path = $value;
+ break;
+ case 't':
+ $target = (int) $value;
+ break;
+ case 'v':
+ $verbose = true;
+ break;
+ }
+ }
+
+ $version = new DBSchemaVersion($domain);
+ $migrator = new Migrator($path, $version, $verbose);
+
+ if ($list) {
+ $migrations = $migrator->relevantMigrations($target);
+
+ foreach ($migrations as $number => $migration) {
+ $description = $migration->description() ?: '(no description)';
+ printf("%3d %s\n", $number, $description);
+ }
+ } elseif ($single) {
+ $direction = 'up';
+ if ($single[0] === '-') {
+ $direction = 'down';
+ $single = substr($single, 1);
+ }
+ $migrator->execute($single, $direction);
+ } else {
+ $migrator->migrateTo($target);
+ }
+}
diff --git a/cli/migrate_help_content.php b/cli/migrate_help_content.php
new file mode 100755
index 0000000..71b4c66
--- /dev/null
+++ b/cli/migrate_help_content.php
@@ -0,0 +1,89 @@
+#!/usr/bin/env php
+<?php
+/**
+* migrate_help_content.php
+*
+* @author Arne Schröder <schroeder@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+* @access public
+*/
+// +---------------------------------------------------------------------------+
+// This file is part of Stud.IP
+//
+// Copyright (C) 2014 Arne Schröder <schroeder@data-quest.de>,
+// Suchi & Berg GmbH <info@data-quest.de>
+// +---------------------------------------------------------------------------+
+// 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 any later version.
+// +---------------------------------------------------------------------------+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+// +---------------------------------------------------------------------------+
+
+require_once __DIR__ . '/studip_cli_env.inc.php';
+
+$help_path = __DIR__ . '/../doc/helpbar';
+
+$argc = $_SERVER['argc'];
+$argv = $_SERVER['argv'];
+
+if (!$argv[2]){
+ fwrite(STDOUT,'Usage: ' . basename(__FILE__) . ' [version] [language]' .chr(10));
+ exit(0);
+}
+
+$query = "SELECT * FROM help_content WHERE studip_version = ? LIMIT 1";
+$statement = DBManager::get()->prepare($query);
+$statement->execute([$argv[1]]);
+$ret = $statement->fetchGrouped(PDO::FETCH_ASSOC);
+if (count($ret)) {
+ trigger_error('Helpbar content already present for this version!', E_USER_ERROR);
+}
+
+$filename = $help_path .'/'. $argv[2] . '/helpcontent.json';
+if (is_file($filename)){
+ $json = json_decode(file_get_contents($filename), true);
+} else {
+ trigger_error("File not found: ".$filename, E_USER_ERROR);
+}
+
+if ($json === null) {
+ trigger_error('Helpbar content could not be loaded. File: '.$filename, E_USER_ERROR);
+}
+
+foreach ($json as $row) {
+ if (!is_array($row['text']))
+ $row['text'] = [$row['text']];
+ if (!$row['label'])
+ $row['label'] = '';
+ if (!$row['icon'])
+ $row['icon'] = '';
+ foreach ($row['text'] as $index => $text) {
+ $count[$argv[2].$row['route']]++;
+ $query = "INSERT INTO help_content (content_id, language, label, icon, content, route, studip_version, position, custom, visible, author_id, installation_id, mkdate)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 1, '', ?, UNIX_TIMESTAMP())";
+ $statement = DBManager::get()->prepare($query);
+ $statement->execute([md5(uniqid(rand(), true)), $argv[2], ($index == 0 ? $row['label'] : ''), ($index == 0 ? $row['icon'] : ''), $text, $row['route'], $argv[1], $count[$argv[2].$row['route']], Config::get()->STUDIP_INSTALLATION_ID]);
+ }
+}
+if (count($count)) {
+ if (!Config::get()->getValue('HELP_CONTENT_CURRENT_VERSION'))
+ Config::get()->create('HELP_CONTENT_CURRENT_VERSION', [
+ 'value' => $argv[1],
+ 'is_default' => 0,
+ 'type' => 'string',
+ 'range' => 'global',
+ 'section' => 'global',
+ 'description' => _('Aktuelle Version der Helpbar-Einträge in Stud.IP')
+ ]);
+ else
+ Config::get()->store('HELP_CONTENT_CURRENT_VERSION', $argv[1]);
+}
+fwrite(STDOUT, 'help content added for '.count($count).' routes.' . chr(10));
+exit(1);
diff --git a/cli/myisam_to_innodb.php b/cli/myisam_to_innodb.php
new file mode 100755
index 0000000..85dab42
--- /dev/null
+++ b/cli/myisam_to_innodb.php
@@ -0,0 +1,122 @@
+#!/usr/bin/env php
+<?php
+require_once(__DIR__.'/studip_cli_env.inc.php');
+
+echo 'Migration starting at '.date('d.m.Y H:i:s').".\n";
+$start = microtime(true);
+
+global $DB_STUDIP_DATABASE;
+
+// Check if InnoDB is enabled in database server.
+$engines = DBManager::get()->fetchAll("SHOW ENGINES");
+$innodb = false;
+foreach ($engines as $e) {
+ // InnoDB is found and enabled.
+ if ($e['Engine'] == 'InnoDB' && in_array(mb_strtolower($e['Support']), ['default', 'yes'])) {
+ $innodb = true;
+ break;
+ }
+}
+
+if ($innodb) {
+ // Get version of database system (MySQL/MariaDB/Percona)
+ $data = DBManager::get()->fetchFirst("SELECT VERSION() AS version");
+ $version = $data[0];
+
+ // Tables to ignore on engine conversion.
+ $ignore_tables = [];
+
+
+
+
+ // Fetch all tables that need to be converted.
+ $tables = DBManager::get()->fetchFirst("SELECT TABLE_NAME
+ FROM `information_schema`.TABLES
+ WHERE TABLE_SCHEMA=:database AND ENGINE=:oldengine
+ ORDER BY TABLE_NAME",
+ [
+ ':database' => $DB_STUDIP_DATABASE,
+ ':oldengine' => 'MyISAM',
+ ]);
+
+ /*
+ * lit_catalog needs fulltext indices which InnoDB doesn't support
+ * in older versions.
+ */
+ if (version_compare($version, '5.6', '<')) {
+ $stmt_fulltext = DBManager::get()->prepare("SHOW INDEX FROM :database.:table WHERE Index_type = 'FULLTEXT'");
+ foreach ($tables as $k => $t) {
+ $stmt_fulltext->bindParam(':table', $t, StudipPDO::PARAM_COLUMN);
+ $stmt_fulltext->bindParam(':database', $DB_STUDIP_DATABASE, StudipPDO::PARAM_COLUMN);
+ $stmt_fulltext->execute();
+ if ($stmt_fulltext->fetch()) {
+ $ignore_tables[] = $t;
+ unset($tables[$k]);
+ }
+ }
+ if (count($ignore_tables)) {
+ echo 'The following tables needs fulltext indices '.
+ 'which are not supported for InnoDB in your database '.
+ 'version, so the tables will be left untouched: ' . join(',', $ignore_tables) . "\n";
+ }
+ }
+
+
+ // Use Barracuda format if database supports it (5.5 upwards).
+ if (version_compare($version, '5.5', '>=')) {
+ echo "\tFound MySQL in version >= 5.5, checking if Barracuda file format is supported...";
+ // Get innodb_file_per_table setting
+ $data = DBManager::get()->fetchOne("SHOW VARIABLES LIKE 'innodb_file_per_table'");
+ $file_per_table = $data['Value'];
+
+ // Check if Barracuda file format is enabled
+ $data = DBManager::get()->fetchOne("SHOW VARIABLES LIKE 'innodb_file_format'");
+ $file_format = $data['Value'];
+
+ if (mb_strtolower($file_per_table) == 'on' && mb_strtolower($file_format) == 'barracuda') {
+ echo " yes.\n";
+ $rowformat = 'DYNAMIC';
+ } else {
+ echo " no:\n";
+ if (mb_strtolower($file_per_table) != 'on') {
+ echo "\t- file_per_table not set\n";
+ }
+ if (mb_strtolower($file_format) != 'barracuda') {
+ echo "\t- file_format not set to Barracuda (but to " . $file_format . ")\n";
+ }
+ $rowformat = 'COMPACT';
+ }
+ }
+
+ // Prepare query for table conversion.
+ $stmt = DBManager::get()->prepare("ALTER TABLE :database.:table ROW_FORMAT=:rowformat ENGINE=:newengine");
+ $stmt->bindParam(':database', $DB_STUDIP_DATABASE, StudipPDO::PARAM_COLUMN);
+ $stmt->bindParam(':rowformat', $rowformat, StudipPDO::PARAM_COLUMN);
+ $newengine = 'InnoDB';
+ $stmt->bindParam(':newengine', $newengine, StudipPDO::PARAM_COLUMN);
+
+ // Now convert the found tables.
+ foreach ($tables as $t) {
+ $local_start = microtime(true);
+ $stmt->bindParam(':table', $t, StudipPDO::PARAM_COLUMN);
+ $stmt->execute();
+ $local_end = microtime(true);
+ $local_duration = $local_end - $local_start;
+ $human_local_duration = sprintf("%02d:%02d:%02d",
+ ($local_duration / 60 / 60) % 24, ($local_duration / 60) % 60, $local_duration % 60);
+
+ echo "\tConversion of table " . $t . " took " . $human_local_duration . ".\n";
+ }
+
+
+ $end = microtime(true);
+
+ $duration = $end - $start;
+ $human_duration = sprintf("%02d:%02d:%02d",
+ ($duration / 60 / 60) % 24, ($duration / 60) % 60, $duration % 60);
+
+ echo 'Migration finished at ' . date('d.m.Y H:i:s') . ', duration ' . $human_duration . ".\n";
+} else {
+ echo "The storage engine InnoDB is not enabled in your ".
+ "database installation, tables cannot be converted.\n";
+}
diff --git a/cli/plugin_manager b/cli/plugin_manager
new file mode 100755
index 0000000..9a065fe
--- /dev/null
+++ b/cli/plugin_manager
@@ -0,0 +1,306 @@
+#!/usr/bin/env php
+<?php
+/*
+ * plugin_manager.php - CLI Plugin-Manager for Stud.IP
+ *
+ * Detailed documentation of this cli-script can be found at:
+ * http://docs.studip.de/develop/Entwickler/CLIPluginManager
+ *
+ * Copyright (C) 2012 - Till Glöggler <tgloeggl@uos.de>
+ *
+ * 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 3 of
+ * the License, or (at your option) any later version.
+ */
+
+require_once 'studip_cli_env.inc.php';
+require_once 'cli/getopts.php';
+
+$args = $_SERVER['argv'];
+
+if ($args) {
+
+ $command = $args[1];
+
+ if (!$command) {
+ echo 'Usage: '. $args[0] .' {install|register|unregister|migrate|activate|deactivate|info|scan}' . "\n";
+ }
+
+ switch ($command) {
+ case 'install':
+ $zipfile = $args[2];
+
+ // show usage
+ if (!$zipfile) {
+ echo 'Usage: '. $args[0] .' install PATH/TO/PLUGIN.ZIP' . "\n\n";
+ exit(1);
+ }
+
+ $plugin_admin = new PluginAdministration();
+
+ try {
+ if (parse_url($zipfile, PHP_URL_SCHEME)) {
+ $plugin_admin->installPluginFromURL($zipfile);
+ } else {
+ $plugin_admin->installPlugin($zipfile);
+ }
+ echo 'Das Plugin wurde erfolgreich installiert.' . "\n";
+ } catch (PluginInstallationException $ex) {
+ echo $ex->getMessage() . "\n";
+ }
+
+ exit(0);
+ break;
+
+ case 'register':
+ $plugindir = $args[2];
+
+ // show usage
+ if (!$plugindir) {
+ echo 'Usage: '. $args[0] .' register PATH/TO/PLUGIN' . "\n\n";
+ # echo 'Options:' . "\n";
+ # echo "\t". '-f force installation and try to (re-)execute any sql-scripts associated' ."\n";
+ exit(1);
+ }
+
+ # $options = getopts(':f'); // if f is set, try to execute the plugins sql-scripts (if any)
+
+ $plugin_manager = PluginManager::getInstance();
+ $manifest = $plugin_manager->getPluginManifest($plugindir);
+
+ if (!$manifest) {
+ echo 'Das Plugin-Manifest fehlt!' . "\n";
+ exit(1);
+ }
+
+ // get plugin meta data
+ $pluginclass = $manifest['pluginclassname'];
+ $origin = $manifest['origin'];
+ $min_version = $manifest['studipMinVersion'];
+ $max_version = $manifest['studipMaxVersion'];
+
+ // check for compatible version
+ if ((isset($min_version) && StudipVersion::olderThan($min_version)) ||
+ (isset($max_version) && StudipVersion::newerThan($max_version))) {
+ throw new PluginInstallationException(_('Das Plugin ist mit dieser Stud.IP-Version nicht kompatibel.'));
+ }
+
+ // determine the plugin path
+ $basepath = Config::get()->PLUGINS_PATH;
+ $pluginpath = $origin . '/' . $pluginclass;
+
+ $plugin_manager = PluginManager::getInstance();
+ $pluginregistered = $plugin_manager->getPluginInfo($pluginclass);
+
+ // create database schema if needed
+ if (isset($manifest['dbscheme']) && !$pluginregistered) {
+ $schemafile = $plugindir . '/' . $manifest['dbscheme'];
+ $contents = file_get_contents($schemafile);
+ $statements = preg_split("/;[[:space:]]*\n/", $contents, -1, PREG_SPLIT_NO_EMPTY);
+ $db = DBManager::get();
+ foreach ($statements as $statement) {
+ $db->exec($statement);
+ }
+ }
+
+ // check for migrations
+ if (is_dir($plugindir . '/migrations')) {
+ $schema_version = new DBSchemaVersion($manifest['pluginname']);
+ $migrator = new Migrator($plugindir . '/migrations', $schema_version);
+ $migrator->migrateTo(null);
+ }
+
+ // now register the plugin in the database
+ $pluginid = $plugin_manager->registerPlugin($manifest['pluginname'], $pluginclass, $pluginpath);
+
+ // register additional plugin classes in this package
+ $additionalclasses = $manifest['additionalclasses'];
+
+ if (is_array($additionalclasses)) {
+ foreach ($additionalclasses as $class) {
+ $plugin_manager->registerPlugin($class, $class, $pluginpath, $pluginid);
+ }
+ }
+
+ echo 'Das Plugin '. $manifest['pluginname'] .' wurde erfolgreich eingetragen.' . "\n";
+ break;
+
+ case 'migrate':
+ $pluginname = $args[2];
+ unset($args[0], $args[1], $args[2]);
+
+ // show usage
+ if (!$pluginname) {
+ echo 'Usage: '. $args[0] .' migrate PLUGINNAME [-l] [-t] [-v]' . "\n";
+ exit(1);
+ }
+
+ // parse options
+ list($errors, $options, $args) = getopts(array('l' => 'Ss l list', 'v' => 'Ss v verbose', 't'=> 'Vs t target'));
+ $list = false;
+ $verbose = false;
+ $target = NULL;
+
+ foreach ($options as $option => $value) {
+ switch ($option) {
+ case 'l':
+ $list = $value;
+ break;
+ case 't':
+ $target = ($value === false) ? null : (int) $value;
+ break;
+ case 'v':
+ $verbose = $value;
+ break;
+ }
+ }
+
+ // create plugin-manager and search for plugin by name
+ $plugin_manager = PluginManager::getInstance();
+ $plugins = $plugin_manager->getPluginInfos();
+
+ foreach ($plugins as $plugin) {
+ if (mb_strtolower($pluginname) === mb_strtolower($plugin['name'])) {
+ $plugindir = Config::get()->PLUGINS_PATH . '/' . $plugin['path'];
+
+ if (is_dir($plugindir . '/migrations')) {
+ // if there are migrations, migrate
+ $schema_version = new DBSchemaVersion($plugin['name']);
+ $migrator = new Migrator($plugindir . '/migrations', $schema_version, $verbose);
+
+ if ($list) {
+ $migrations = $migrator->relevantMigrations($target);
+
+ foreach ($migrations as $number => $migration) {
+ $description = $migration->description() ?: '(no description)';
+
+ printf("%3d %-20s %s\n", $number, get_class($migration), $description);
+ }
+ } else {
+ $migrator->migrateTo($target);
+ }
+
+ exit(0);
+ } else {
+ echo 'Konnte keine Migrationen für das Plugin '. $plugin['name'] .' finden.' . "\n";
+ exit(1);
+ }
+ }
+ }
+
+ echo 'Konnte kein Plugin mit dem Namen ' . $pluginname . ' finden.' . "\n";
+ echo 'Überprüfen sie bitte den Namen (auch auf Groß-/Kleinschreibung!)' ."\n";
+ exit(1);
+ break;
+
+ case 'unregister':
+ $pluginname = $args[2];
+
+ // show usage
+ if (!$pluginname) {
+ echo 'Usage: '. $args[0] .' unregister PLUGINNAME' . "\n";
+ exit(1);
+ }
+
+ $plugin_manager = PluginManager::getInstance();
+ $plugins = $plugin_manager->getPluginInfos();
+ foreach ($plugins as $plugin) {
+ if (mb_strtolower($pluginname) == mb_strtolower($plugin['name'])) {
+ $plugindir = Config::get()->PLUGINS_PATH .'/'. $plugin['path'];
+
+ $plugin_manager->unregisterPlugin($plugin['id']);
+
+ if (is_dir($plugindir . '/migrations')) {
+ $schema_version = new DBSchemaVersion($plugin['name']);
+ $migrator = new Migrator($plugindir . '/migrations', $schema_version);
+ $migrator->migrate_to(0);
+ }
+
+ echo 'Das Plugin '. $plugin['name'] .' wurde ausgetragen.' . "\n";
+ exit(0);
+ }
+ }
+
+ echo 'Konnte kein Plugin mit dem Namen '. $pluginname .' finden.' . "\n";
+ echo 'Überprüfen sie bitte den Namen (auch auf Groß-/Kleinschreibung!)' ."\n";
+ exit(1);
+ break;
+
+ case 'activate':
+ case 'deactivate':
+ $pluginname = $args[2];
+
+ // show usage
+ if (!$pluginname) {
+ echo 'Usage: '. $args[0] .' '. $command .' PLUGINNAME' . "\n";
+ exit(1);
+ }
+
+ $plugin_manager = PluginManager::getInstance();
+ $plugins = $plugin_manager->getPluginInfos();
+ foreach ($plugins as $plugin) {
+ if (mb_strtolower($pluginname) == mb_strtolower($plugin['name'])) {
+ $plugin_manager->setPluginEnabled($plugin['id'], ($command == 'activate'));
+ echo 'Das Plugin '. $plugin['name'] .' wurde ' . ($command == 'activate' ? 'aktiviert' : 'deaktiviert') . '.' . "\n";
+ exit(0);
+ }
+ }
+
+ echo 'Konnte kein Plugin mit dem Namen '. $pluginname .' finden.' . "\n";
+ echo 'Überprüfen sie bitte den Namen (auch auf Groß-/Kleinschreibung!)' ."\n";
+ exit(1);
+ break;
+
+ case 'info':
+ $pluginname = $args[2];
+
+ $plugin_manager = PluginManager::getInstance();
+ $plugins = $plugin_manager->getPluginInfos();
+ if ($pluginname) {
+ $plugins = array_filter($plugins, function($p) use ($pluginname) {return mb_stripos($p['name'], $pluginname) !== false;});
+ }
+ $basepath = Config::get()->PLUGINS_PATH;
+ foreach ($plugins as $plugin) {
+ $plugindir = $basepath . '/' . $plugin['path'] . '/';
+ $plugin['class_exists'] = 0;
+ $pluginfile = $plugindir . $plugin['class'] . '.class.php';
+ if (file_exists($pluginfile)) {
+ $plugin['class_exists'] = 1;
+ } else {
+ $pluginfile = $plugindir . $plugin['class'] . '.php';
+ if (file_exists($pluginfile)) {
+ $plugin['class_exists'] = 1;
+ }
+ }
+ if (is_dir($plugindir . '/migrations')) {
+ $schema_version = new DBSchemaVersion($plugin['name']);
+ $migrator = new Migrator($plugindir .'/migrations', $schema_version);
+ $plugin['migration_top_version'] = $migrator->topVersion();
+ $plugin['schema_version'] = $schema_version->get();
+ }
+ echo "\n";
+ $plugin['type'] = join(',' , $plugin['type']);
+ echo join("\n", array_filter(array_map(function($p){if ($p[0] == ' ') return trim($p);},explode("\n", print_r($plugin,1)))));
+ echo "\n";
+ }
+ exit(0);
+ break;
+
+ case 'scan':
+ $plugin_admin = new PluginAdministration();
+ $plugin_manager = PluginManager::getInstance();
+ foreach ($plugin_admin->scanPluginDirectory() as $manifest) {
+ if (!$plugin_manager->getPluginInfo($manifest['pluginclassname'])) {
+ echo "\n";
+ echo join("\n", array_filter(array_map(function($p){if ($p[0] == ' ') return trim($p);},explode("\n", print_r($manifest,1)))));
+ echo "\n";
+ }
+ }
+ exit(0);
+ break;
+ }
+
+}
+
+exit(0);
diff --git a/cli/studip-compat.php b/cli/studip-compat.php
new file mode 100755
index 0000000..4abc67c
--- /dev/null
+++ b/cli/studip-compat.php
@@ -0,0 +1,203 @@
+#!/usr/bin/env php
+<?php
+require_once 'studip_cli_env.inc.php';
+
+$opts = getopt('fhnvc', ['filenames', 'help', 'non-recursive', 'verbose', 'no-color']);
+
+if (isset($opts['h']) || isset($opts['help'])) {
+ fwrite(STDOUT, 'Stud.IP compatibility scanner - Checks plugins for common issues' . PHP_EOL);
+ fwrite(STDOUT, '================================================================' . PHP_EOL);
+ fwrite(STDOUT, 'Usage: ' . basename(__FILE__) . ' [OPTION] [VERSION] [FOLDER] ..' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ fwrite(STDOUT, '[VERSION] is optional, if not given all checks are applied.' . PHP_EOL);
+ fwrite(STDOUT, '[FOLDER] will default to the plugins_packages folder.' . PHP_EOL);
+ fwrite(STDOUT, 'Supply as many folders as you need.' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ fwrite(STDOUT, 'Options:' . PHP_EOL);
+ fwrite(STDOUT, ' -h, --help Display this help' . PHP_EOL);
+ fwrite(STDOUT, ' -f, --filenames Display only filenames' . PHP_EOL);
+ fwrite(STDOUT, ' -n, --non-recursive Do not scan recursively into subfolders' . PHP_EOL);
+ fwrite(STDOUT, ' -c, --no-color Do not use colors for output' . PHP_EOL);
+ fwrite(STDOUT, ' -v, --verbose Print additional information' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ exit(0);
+}
+
+// Reduce arguments by options (this is far from perfect)
+$args = $_SERVER['argv'];
+$arg_stop = array_search('--', $args);
+if ($arg_stop !== false) {
+ $args = array_slice($args, $arg_stop + 1);
+} elseif (count($opts)) {
+ $args = array_slice($args, 1 + count($opts));
+} else {
+ $args = array_slice($args, 1);
+}
+
+$verbose = isset($opts['v']) || isset($opts['verbose']);
+$only_filenames = isset($opts['f']) || isset($opts['filenames']);
+$recursive = !(isset($opts['n']) || isset($opts['non-recursive']));
+$no_colors = isset($opts['c']) || isset($opts['no-color']) || !stream_isatty(STDOUT);
+$version = null;
+$folders = array_values($args) ?: [];
+
+if (count($folders) > 0 && preg_match('/^\d+\.\d+$/', $folders[0])) {
+ $version = array_shift($folders);
+}
+
+// Prepare logging mechanism
+$log = function ($message) use ($no_colors) {
+ $ansi = [
+ 'off' => 0,
+ 'bold' => 1,
+ 'italic' => 3,
+ 'underline' => 4,
+ 'blink' => 5,
+ 'inverse' => 7,
+ 'hidden' => 8,
+ 'black' => 30,
+ 'red' => 31,
+ 'green' => 32,
+ 'yellow' => 33,
+ 'blue' => 34,
+ 'magenta' => 35,
+ 'cyan' => 36,
+ 'white' => 37,
+ 'black_bg' => 40,
+ 'red_bg' => 41,
+ 'green_bg' => 42,
+ 'yellow_bg' => 43,
+ 'blue_bg' => 44,
+ 'magenta_bg' => 45,
+ 'cyan_bg' => 46,
+ 'white_bg' => 47
+ ];
+
+ $message = trim($message);
+
+ if ($message) {
+ $args = array_slice(func_get_args(), 1);
+ $message = vsprintf($message . "\n", $args);
+
+ $ansi_codes = implode('|', array_keys($ansi));
+ if (preg_match_all('/#\{((?:(?:' . $ansi_codes . '),?)+):(.+?)\}/s', $message, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $chunk = '';
+ if (!$no_colors) {
+ $codes = explode(',', $match[1]);
+ foreach ($codes as $code) {
+ $chunk .= "\033[{$ansi[$code]}m";
+ }
+ }
+ $chunk .= $match[2];
+ if (!$no_colors) {
+ $chunk .= "\033[{$ansi[off]}m";
+ }
+
+ $message = str_replace($match[0], $chunk, $message);
+ }
+ }
+
+ print $message;
+ }
+};
+$log_if = function ($condition, $message) use ($log) {
+ if ($condition) {
+ call_user_func_array($log, array_slice(func_get_args(), 1));
+ }
+};
+
+// Reduces filename by base path and plugin folder
+$reduce = function ($folder) {
+ $folder = str_replace($GLOBALS['STUDIP_BASE_PATH'] . '/', '', $folder);
+ $folder = str_replace('public/plugins_packages/', '', $folder);
+ return $folder;
+};
+
+// Get rules
+if (!$version) {
+ $rules = [];
+ foreach (glob(__DIR__ . '/compatbility-rules/*.php') as $file) {
+ $version_rules = require $file;
+ $rules = array_merge($rules, $version_rules);
+ }
+} elseif (!file_exists(__DIR__ . "/compatibility-rules/studip-{$version}.php")) {
+ $log('#{red:No rules defined for Stud.IP version %s}', $version);
+ die;
+} else {
+ $rules = require __DIR__ . "/compatibility-rules/studip-{$version}.php";
+}
+
+// Prepare folders
+if (count($folders) === 0) {
+ $folders = rtrim($GLOBALS['STUDIP_BASE_PATH'], '/') . '/public/plugins_packages';
+ $folders = glob($folders . '/*/*');
+}
+$folders = array_unique($folders);
+
+$checkRule = function ($rule, $contents) {
+ if ($rule[0] === '/' && $rule[strlen($rule) - 1] === '/') {
+ return (bool) preg_match("{$rule}s", $contents);
+ }
+
+ return strpos($contents, strtolower($rule)) > 0;
+};
+
+// Main checker
+$check = function ($filename) use ($checkRule, $rules) {
+ $errors = [];
+
+ $contents = strtolower(file_get_contents($filename));
+ foreach ($rules as $needle => $suggestion) {
+ if ($checkRule($needle, $contents)) {
+ $errors[$needle] = $suggestion;
+ }
+ }
+ return $errors;
+};
+
+// Engage
+foreach ($folders as $folder) {
+ if (!file_exists($folder) || !is_dir($folder)) {
+ $log_if($verbose, 'Skipping non-folder arg #{red:%s}', $folder);
+ continue;
+ }
+
+ $log_if($verbose && !$only_filenames, '#{green:Scanning} %s', $reduce($folder));
+ if ($recursive) {
+ $iterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::UNIX_PATHS);
+ $iterator = new RecursiveIteratorIterator($iterator);
+ } else {
+ $iterator = new DirectoryIterator($folder);
+ }
+ $regexp_iterator = new RegexIterator($iterator, '/.*\.(?:php|tpl|inc|js)$/', RecursiveRegexIterator::MATCH);
+
+ $issues = [];
+
+ foreach ($regexp_iterator as $file) {
+ $filename = $file->getPathName();
+ $log_if($verbose, "Checking #{magenta:%s}", $filename);
+ if ($errors = $check($filename)) {
+ $issues[$filename] = $errors;
+ }
+ }
+
+ if (count($issues) > 0) {
+ $issue_count = array_sum(array_map('count', $issues));
+ $message = count($issues) === 1
+ ? '#{red:%u issue found in} #{red,bold:%s}'
+ : '#{red:%u issues found in} #{red,bold:%s}';
+ $log_if(!$only_filenames, $message, $issue_count, $reduce($folder));
+
+ foreach ($issues as $filename => $errors) {
+ if ($only_filenames) {
+ $log($filename);
+ } else {
+ $log('> File #{green,bold:%s}', $reduce($filename));
+ foreach ($errors as $needle => $suggestion) {
+ $log('- #{cyan:%s} -> %s', $needle, $suggestion ?: '#{red:No suggestion available}');
+ }
+ }
+ }
+ }
+}
diff --git a/cli/studip_cli_env.inc.php b/cli/studip_cli_env.inc.php
new file mode 100644
index 0000000..a0e33cd
--- /dev/null
+++ b/cli/studip_cli_env.inc.php
@@ -0,0 +1,80 @@
+<?php
+# Lifter007: TODO
+# Lifter003: TODO
+/**
+* studip_cli_env.inc.php
+*
+* sets up a faked Stud.IP environment with usable $auth, $user and $perm objects
+* for a faked 'root' user, sets custom error handler wich writes to STDERR
+*
+* @author André Noack <noack@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+* @access public
+*/
+// +---------------------------------------------------------------------------+
+// This file is part of Stud.IP
+// studip_cli_env.inc.php
+//
+// Copyright (C) 2006 André Noack <noack@data-quest.de>,
+// Suchi & Berg GmbH <info@data-quest.de>
+// +---------------------------------------------------------------------------+
+// 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 any later version.
+// +---------------------------------------------------------------------------+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+// +---------------------------------------------------------------------------+
+
+function CliErrorHandler($errno, $errstr, $errfile, $errline) {
+ if ($errno & ~(E_NOTICE | E_STRICT | E_DEPRECATED | E_WARNING | E_USER_WARNING | E_USER_NOTICE | E_USER_DEPRECATED) && error_reporting()){
+ fwrite(STDERR,"$errstr \n$errfile line $errline\n");
+ exit(1);
+ }
+ return true;
+}
+
+function parse_msg_to_clean_text($long_msg,$separator="§") {
+ $msg = explode ($separator,$long_msg);
+ $ret = [];
+ for ($i=0; $i < count($msg); $i=$i+2) {
+ if ($msg[$i+1]) $ret[] = trim(decodeHTML(preg_replace ("'<[\/\!]*?[^<>]*?>'si", "", $msg[$i+1])));
+ }
+ return join("\n", $ret);
+}
+
+$STUDIP_BASE_PATH = realpath( dirname(__FILE__) . '/..');
+$include_path = get_include_path();
+$include_path .= PATH_SEPARATOR . $STUDIP_BASE_PATH . DIRECTORY_SEPARATOR . 'public';
+set_include_path($include_path);
+set_error_handler('CliErrorHandler');
+
+require_once $STUDIP_BASE_PATH . "/lib/bootstrap.php";
+
+// disable caching for cli scripts
+$GLOBAL_CACHING_ENABLE = $GLOBALS['CACHING_ENABLE'];
+$CACHING_ENABLE = false;
+
+// set base url for URLHelper class
+URLHelper::setBaseUrl($ABSOLUTE_URI_STUDIP);
+
+//cli scripts run always as faked (Stud.IP) root
+$auth = new Seminar_Auth();
+$auth->auth = ['uid' => 'cli',
+ 'uname' => 'cli',
+ 'perm' => 'root'];
+
+$faked_root = new User();
+$faked_root->user_id = 'cli';
+$faked_root->username = 'cli';
+$faked_root->perms = 'root';
+$user = new Seminar_User($faked_root);
+unset($faked_root);
+
+$perm = new Seminar_Perm();
+?>
diff --git a/cli/tic_5671_scan.php b/cli/tic_5671_scan.php
new file mode 100755
index 0000000..07fa5c2
--- /dev/null
+++ b/cli/tic_5671_scan.php
@@ -0,0 +1,172 @@
+#!/usr/bin/env php
+<?php
+require_once 'studip_cli_env.inc.php';
+
+$opts = getopt('fhnosv', ['filenames', 'help', 'non-recursive', 'occurences', 'matches', 'verbose']);
+
+if (isset($opts['h']) || isset($opts['help'])) {
+ fwrite(STDOUT, 'TIC 5671 Scanner - Scans files for occurences of globalized config items' . PHP_EOL);
+ fwrite(STDOUT, '========================================================================' . PHP_EOL);
+ fwrite(STDOUT, 'Usage: ' . basename(__FILE__) . ' [OPTION] [FOLDER] [FOLDER2] ..' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ fwrite(STDOUT, '[FOLDER] will default to Stud.IP base folder.' . PHP_EOL);
+ fwrite(STDOUT, 'Supply many folders if you need to.' . PHP_EOL);
+ fwrite(STDOUT, 'You may pass the special value of "plugins" to scan the plugin folder.' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ fwrite(STDOUT, 'Options:' . PHP_EOL);
+ fwrite(STDOUT, ' -h, --help Display this help' . PHP_EOL);
+ fwrite(STDOUT, ' -f, --filenames Display only filenames (excludes -m and -o)' . PHP_EOL);
+ fwrite(STDOUT, ' -n, --non-recursive Do not scan recursively into subfolders' . PHP_EOL);
+ fwrite(STDOUT, ' -m, --matches Show matched config variables' . PHP_EOL);
+ fwrite(STDOUT, ' -o, --occurences Display occurences in files (implies -s)' . PHP_EOL);
+ fwrite(STDOUT, ' -v, --verbose Print additional information' . PHP_EOL);
+ fwrite(STDOUT, PHP_EOL);
+ exit(0);
+}
+
+// Reduce arguments by options (this is far from perfect)
+$args = $_SERVER['argv'];
+$arg_stop = array_search('--', $args);
+if ($arg_stop !== false) {
+ $args = array_slice($args, $arg_stop + 1);
+} elseif (count($opts)) {
+ $args = array_slice($args, 1 + count($opts));
+} else {
+ $args = array_slice($args, 1);
+}
+
+$verbose = isset($opts['v']) || isset($opts['verbose']);
+$only_filenames = isset($opts['f']) || isset($opts['filenames']);
+$show_occurences = $verbose || isset($opts['o']) || isset($opts['occurences']);
+$show_matches = $show_occurences || isset($opts['m']) || isset($opts['matches']);
+$recursive = !(isset($opts['n']) || isset($opts['recursive']));
+$folders = $args ?: [$GLOBALS['STUDIP_BASE_PATH']];
+
+// Prepare logging mechanism
+$log = function ($message) {
+ $ansi = [
+ 'off' => 0,
+ 'bold' => 1,
+ 'italic' => 3,
+ 'underline' => 4,
+ 'blink' => 5,
+ 'inverse' => 7,
+ 'hidden' => 8,
+ 'black' => 30,
+ 'red' => 31,
+ 'green' => 32,
+ 'yellow' => 33,
+ 'blue' => 34,
+ 'magenta' => 35,
+ 'cyan' => 36,
+ 'white' => 37,
+ 'black_bg' => 40,
+ 'red_bg' => 41,
+ 'green_bg' => 42,
+ 'yellow_bg' => 43,
+ 'blue_bg' => 44,
+ 'magenta_bg' => 45,
+ 'cyan_bg' => 46,
+ 'white_bg' => 47
+ ];
+
+ $message = trim($message);
+
+ if ($message) {
+ $ansi_codes = implode('|', array_keys($ansi));
+ if (preg_match_all('/#\{((?:(?:' . $ansi_codes . '),?)+):(.+?)\}/s', $message, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $chunk = '';
+ $codes = explode(',', $match[1]);
+ foreach ($codes as $code) {
+ $chunk .= "\033[{$ansi[$code]}m";
+ }
+ $chunk .= $match[2] . "\033[{$ansi[off]}m";
+
+ $message = str_replace($match[0], $chunk, $message);
+ }
+ }
+
+ $args = array_slice(func_get_args(), 1);
+ vprintf($message . "\n", $args);
+ }
+};
+$log_if = function ($condition, $message) use ($log) {
+ if ($condition) {
+ call_user_func_array($log, array_slice(func_get_args(), 1));
+ }
+};
+
+// Prepare line highlighter
+$highlight = function ($content, $variable) {
+ $lines = explode("\n", $content);
+
+ $result = [];
+ foreach ($lines as $index => $line) {
+ if (mb_strpos($line, $variable) === false) {
+ continue;
+ }
+ $result[$index + 1] = $line;
+ }
+
+ if (!$result) {
+ return '';
+ }
+
+ $max = max(array_map('mb_strlen', array_keys($result)));
+
+ foreach ($result as $index => $line) {
+ $result[$index] = sprintf('#{yellow:%0' . $max . 'u}: %s', $index, str_replace($variable, "#{yellow_bg,black:$variable}", $line));
+ }
+
+ return implode("\n", $result);
+};
+
+// Prepare folders
+foreach ($folders as $index => $folder) {
+ if ($folder === 'plugins') {
+ $folders[$index] = $GLOBALS['STUDIP_BASE_PATH'] . '/public/plugins_packages/';
+ }
+}
+$folders = array_unique($folders);
+
+// Prepare regexp from regexp
+$config = Config::get()->getFields('global');
+$quoted = array_map(function ($item) { return preg_quote($item, '/'); }, $config);
+$regexp = '/\$(?:GLOBALS\[["\']?)?(' . implode('|', $quoted) . ')\b/S';
+
+// Engage
+foreach ($folders as $folder) {
+ if (!file_exists($folder) || !is_dir($folder)) {
+ $log_if($verbose, 'Skipping non-folder arg #{red:%s}', $folder);
+ continue;
+ }
+ $log_if($verbose, 'Scanning "%s"', $folder);
+ if ($recursive) {
+ $iterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::UNIX_PATHS);
+ $iterator = new RecursiveIteratorIterator($iterator);
+ } else {
+ $iterator = new DirectoryIterator($folder);
+ }
+ $regexp_iterator = new RegexIterator($iterator, '/.*\.(?:php|tpl|inc)$/', RecursiveRegexIterator::MATCH);
+
+ foreach ($regexp_iterator as $file) {
+ $filename = $file->getPathName();
+ $contents = file_get_contents($filename);
+ $log_if($verbose, "Checking #{magenta:%s}", $filename);
+ if ($matched = preg_match_all($regexp, $contents, $matches)) {
+ if ($only_filenames) {
+ $log($filename);
+ } else {
+ $log('%u matched variable(s) in #{green,bold:%s}', $matched, $filename);
+ if ($show_matches) {
+ $variables = array_unique($matches[1]);
+ foreach ($variables as $variable) {
+ $log('>> #{cyan:%s}', $variable);
+ $log_if($show_occurences, $highlight($contents, $variable));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/cli/update-resource-booking-intervals.php b/cli/update-resource-booking-intervals.php
new file mode 100644
index 0000000..50c6e4b
--- /dev/null
+++ b/cli/update-resource-booking-intervals.php
@@ -0,0 +1,33 @@
+#!/usr/bin/env php
+<?php
+
+
+require_once(__DIR__ . '/studip_cli_env.inc.php');
+
+
+$keep_exceptions = true;
+
+$options = getopt('h', ['remove-exceptions']);
+
+if (array_key_exists('h', $options)) {
+ echo("Usage:\tupdate-resource-booking-intervals.php [--remove-exceptions]\n");
+ echo("\tIf --remove-exceptions is set, exceptions for a booking with repetitions\n");
+ echo("\twill be removed. By default, they are kept.\n");
+ exit(0);
+}
+
+if (array_key_exists('remove-exceptions', $options)) {
+ $keep_exceptions = false;
+ echo("Exceptions in bookings with repetitions will be removed!\n");
+}
+
+$bookings = ResourceBooking::findBySql('TRUE');
+if (!$bookings) {
+ echo("There are no bookings in your database! Nothing to do!\n");
+ exit(0);
+}
+foreach ($bookings as $booking) {
+ $booking->updateIntervals($keep_exceptions);
+}
+
+echo("End of script. The resource_booking_intervals table is up to date again!\n");
diff --git a/cli/vue-gettext-split-translations.php b/cli/vue-gettext-split-translations.php
new file mode 100755
index 0000000..f2b2ec8
--- /dev/null
+++ b/cli/vue-gettext-split-translations.php
@@ -0,0 +1,16 @@
+#!/usr/bin/env php
+<?php
+
+$translationsFile = realpath(__DIR__ . '/../resources/locales/translations.json');
+if (!file_exists($translationsFile)) {
+ fwrite(STDERR, "Could not find translations in '" . $translationsFile . "'.\n");
+ exit(1);
+}
+
+$file = file_get_contents($translationsFile);
+$json = json_decode($file, true);
+
+foreach ($json as $lang => $content) {
+ $langFile = realpath(__DIR__ . '/../resources/locales/') . '/' . $lang . '.json';
+ file_put_contents($langFile, json_encode($content));
+}