current_action = $action; // allow only "word" characters in arguments $this->validate_args($args); parent::before_filter($action, $args); if ($this->with_session) { # open session page_open([ 'sess' => 'Seminar_Session', 'auth' => $this->allow_nobody ? 'Seminar_Default_Auth' : 'Seminar_Auth', 'perm' => 'Seminar_Perm', 'user' => 'Seminar_User' ]); // show login-screen, if authentication is "nobody" $GLOBALS['auth']->login_if((Request::get('again') || !$this->allow_nobody) && $GLOBALS['user']->id == 'nobody'); // Setup flash instance $this->flash = Trails_Flash::instance(); // set up user session include 'lib/seminar_open.php'; } // Set generic attribute that indicates whether the request was sent // via ajax or not $this->via_ajax = Request::isXhr(); # Set base layout # # If your controller needs another layout, overwrite your controller's # before filter: # # class YourController extends AuthenticatedController { # function before_filter(&$action, &$args) { # parent::before_filter($action, $args); # $this->set_layout("your_layout"); # } # } # # or unset layout by sending: # # $this->set_layout(NULL) # $layout_file = Request::isXhr() ? 'layouts/dialog.php' : 'layouts/base.php'; $layout = $GLOBALS['template_factory']->open($layout_file); $this->set_layout($layout); $this->set_content_type('text/html;charset=utf-8'); } /** * Extended method to inject extended response object. */ public function erase_response() { parent::erase_response(); $this->response = new StudipResponse(); } /** * Hooked perform method in order to inject body element id creation. * * In order to avoid clashes, these body element id will be joined * with a minus sign. Otherwise the controller "x" with action * "y_z" would be given the same id as the controller "x/y" with * the action "z", namely "x_y_z". With the minus sign this will * result in the ids "x-y_z" and "x_y-z". * * Plugins will always have a leading 'plugin-' and the decamelized * plugin name in front of the id. * * @param String $unconsumed_path Path segment containing action and * optionally arguments or format * @return Trails_Response from parent controller */ public function perform($unconsumed_path) { // Set body element id if it has not already been set if (!PageLayout::hasBodyElementId()) { $body_id = $this->getBodyElementIdForControllerAndAction($unconsumed_path); PageLayout::setBodyElementId($body_id); } return parent::perform($unconsumed_path); } /** * Callback function being called after an action is executed. * * @param string Name of the action to perform. * @param array An array of arguments to the action. * * @return void */ public function after_filter($action, $args) { parent::after_filter($action, $args); if (Request::isXhr() && !isset($this->response->headers['X-Title']) && PageLayout::hasTitle()) { $this->response->add_header('X-Title', rawurlencode(PageLayout::getTitle())); } if (Request::isXhr() && !isset($this->response->headers['X-WikiLink']) && PageLayout::getHelpKeyword()) { $this->response->add_header('X-WikiLink', format_help_url(PageLayout::getHelpKeyword())); } if ($this->with_session) { page_close(); } } /** * Validate arguments based on a list of given types. The types are: * 'int', 'float', 'option'. If the list of types is NULL * or shorter than the argument list, 'option' is assumed for all * remaining arguments. 'option' differs from Request::option() in * that it also accepts the charaters '-' and ',' in addition to all * word charaters. * * Since Stud.IP 4.0 it is also possible to directly inject * SimpleORMap objects. If types is NULL, the signature of the called * action is analyzed and any type hint that matches a sorm class * will be used to create an object using the argument as the id * that is passed to the object's constructor. * * If $_autobind is set to true, the created object is also assigned * to the controller so that it is available in a view. * * @param array an array of arguments to the action * @param array list of argument types (optional) */ public function validate_args(&$args, $types = null) { $class_infos = []; if ($types === null) { $types = array_fill(0, count($args), 'option'); } if ($this->has_action($this->current_action)) { $reflection = new ReflectionMethod($this, $this->current_action . '_action'); $parameters = $reflection->getParameters(); foreach ($parameters as $i => $parameter) { $class_type = $parameter->getClass(); if (!$class_type || !class_exists($class_type->name) || !is_a($class_type->name, SimpleORMap::class, true)) { continue; } $types[$i] = 'sorm'; $class_infos[$i] = [ 'model' => $class_type->name, 'var' => $parameter->getName(), 'optional' => $parameter->isOptional(), ]; if ($parameter->isOptional() && !isset($args[$i])) { $args[$i] = $parameter->getDefaultValue(); } } } foreach ($args as $i => &$arg) { $type = $types[$i] ?: 'option'; switch ($type) { case 'int': $arg = (int) $arg; break; case 'float': $arg = (float) strtr($arg, ',', '.'); break; case 'option': if (preg_match('/[^\\w,-]/', $arg)) { throw new Trails_Exception(400); } break; case 'sorm': $info = $class_infos[$i]; $id = null; if ($arg != -1) { $id = $arg; } if (mb_strpos($id, SimpleORMap::ID_SEPARATOR) !== false) { $id = explode(SimpleORMap::ID_SEPARATOR, $id); } $reflection = new ReflectionClass($info['model']); $sorm = $reflection->newInstance($id); if (!$info['optional'] && $sorm->isNew()) { throw new Trails_Exception( 404, "Parameter {$info['var']} could not be resolved with value {$arg}" ); } $arg = $sorm; if ($this->_autobind) { $this->{$info['var']} = $arg; } break; default: throw new Trails_Exception(500, 'Unknown type "' . $type . '"'); } } reset($args); } /** * Returns a URL to a specified route to your Trails application. * without first parameter the current action is used * if route begins with a / then the current controller ist prepended * if second parameter is an array it is passed to URLHeper * * @param string a string containing a controller and optionally an action * @param string[] optional arguments * * @return string a URL to this route */ public function url_for($to = ''/* , ... */) { $args = func_get_args(); // Try to create route if none given if ($to === '') { $args[0] = isset($this->parent_controller) ? $this->parent_controller->current_action : $this->current_action; return $this->action_url(...$args); } // Create url for a specific action // TODO: This seems odd. You kinda specify an absolute path // to receive a relative url. Meh... // // @deprecated Do not use this, please! if ($to[0] === '/') { $args[0] = substr($to, 1); return $this->action_url(...$args); } // Check for absolute URL if ($this->isURL($to)) { throw new InvalidArgumentException(__METHOD__ . ' cannot be used with absolute URLs'); } // Extract fragment (if any) if (strpos($to, '#') !== false) { list($args[0], $fragment) = explode('#', $to); } // Extract parameters (if any) $params = []; if (is_array(end($args))) { $params = array_pop($args); } // Map any sorm objects to their ids $args = array_map(function ($arg) { if (is_object($arg) && $arg instanceof SimpleORMap) { return $arg->isNew() ? -1 : $arg->id; } return $arg; }, $args); $url = parent::url_for(...$args); if (isset($fragment)) { $url .= '#' . $fragment; } return URLHelper::getURL($url, $params); } /** * Returns an escaped URL to a specified route to your Trails application. * without first parameter the current action is used * if route begins with a / then the current controller ist prepended * if second parameter is an array it is passed to URLHeper * * @param string a string containing a controller and optionally an action * @param strings optional arguments * * @return string a URL to this route */ public function link_for($to = ''/* , ... */) { return htmlReady($this->url_for(...func_get_args())); } /** * Redirects the user another page. Accepts multiple parameters just like * url_for(). * * @param string $to * @see StudipController::url_for() */ public function redirect($to) { $to = $this->adjustToArguments(...func_get_args()); parent::redirect($to); } /** * Relocate the user to another location. This is a specialized version * of redirect that differs in two points: * * - relocate() will force the browser to leave the current dialog while * redirect would refresh the dialog's contents * - relocate() accepts all the parameters that url_for() accepts so it's * no longer neccessary to chain url_for() and redirect() * * @param String $to Location to redirect to */ public function relocate($to) { $to = $this->adjustToArguments(...func_get_args()); if (Request::isDialog()) { $this->response->add_header('X-Location', encodeURI($to)); $this->render_nothing(); } else { parent::redirect($to); } } /** * Returns a URL to a specified route to your Trails application, unless * the parameter is already a valid URL (which is returned unchanged). * * If no absolute url or more than one argument is given, url_for() is * used. */ private function adjustToArguments(...$args): string { if (count($args) > 1 && $this->isURL($args[0])) { throw new InvalidArgumentException('Method may not be used with a URL and multiple parameters'); } if (count($args) === 1 && $this->isURL($args[0])) { return $args[0]; } return $this->url_for(...$args); } /** * Returns whether the given parameter is a valid url. * * @param string $to * @return bool */ private function isURL(string $to): bool { return preg_match('#^(/|\w+://)#', $to); } /** * Exception handler called when the performance of an action raises an * exception. * * @param object the thrown exception */ public function rescue($exception) { throw $exception; } /** * render given data as json, data is converted to utf-8 * * @param mixed $data */ public function render_json($data) { $this->set_content_type('application/json;charset=utf-8'); $this->render_text(json_encode($data)); } /** * Render given data as csv, data is assumed to be utf-8. * The first row of data may contain column titles. * * @param array $data data as two dimensional array * @param string $filename download file name (optional) * @param string $delimiter field delimiter char (optional) * @param string $enclosure field enclosure char (optional) */ public function render_csv($data, $filename = null, $delimiter = ';', $enclosure = '"') { $this->set_content_type('text/csv; charset=UTF-8'); $output = fopen('php://temp', 'rw'); fputs($output, "\xEF\xBB\xBF"); foreach ($data as $row) { fputcsv($output, $row, $delimiter, $enclosure); } rewind($output); $csv_data = stream_get_contents($output); fclose($output); if (isset($filename)) { $this->response->add_header('Content-Disposition', 'attachment; ' . encode_header_parameter('filename', $filename)); } $this->response->add_header('Content-Length', strlen($csv_data)); $this->render_text($csv_data); } /** * Renders a pdf file given by a TCPDF/ExportPDF object. * * @param TCPDF $pdf TCPDF object to render * @param string $filename Filename * @param bool $inline Should the pdf be displayed inline (default: no) */ protected function render_pdf(TCPDF $pdf, $filename, $inline = false) { $temp_file = $GLOBALS['TMP_PATH'] . '/' . md5(uniqid('pdf-file', true)); $pdf->Output($temp_file, 'F'); $disposition = $inline ? 'inline' : 'attachment'; $this->render_temporary_file($temp_file, $filename, 'application/pdf', $disposition); } /** * Renders a file * @param string $file Path of the file to render * @param string $filename Name of the file displayed to user * (will equal $file when missing) * @param string $content_type Optional content type (will be determined if missing) * @param string $content_disposition Either attachment (default) or inline * @param Closure $callback Optional callback when download has finished * @param int $chunk_size Optional size of chunks to send (default: 256k) */ public function render_file( $file, $filename = null, $content_type = null, $content_disposition = 'attachment', Closure $callback = null, $chunk_size = 262144 ) { if (!file_exists($file)) { throw new Trails_Exception(404); } if (!is_readable($file)) { throw new Trails_Exception(500); } if ($content_type === null) { $finfo = finfo_open(FILEINFO_MIME_TYPE); $content_type = finfo_file($finfo, $file); } $this->set_content_type($content_type); $this->response->add_header( 'Content-Disposition', "{$content_disposition}; " . encode_header_parameter( 'filename', FileManager::cleanFileName($filename ?: basename($file)) ) ); $this->response->add_header('Content-Length', filesize($file)); $this->response->add_header('Content-Transfer-Encoding', 'binary'); $this->response->add_header('Pragma', 'public'); $this->render_text(function () use ($file, $chunk_size, $callback) { $fp = fopen($file, 'rb'); while (!feof($fp)) { yield fgets($fp, $chunk_size); } fclose($fp); if ($callback) { $callback($file); } }); } /** * Renders a temporary file which will be deleted after transmission. * This is just a convenience method so you don't have to write the delete * callback. * * @param string $file Path of the file to render * @param string $filename Name of the file displayed to user * (will equal $file when missing) * @param string $content_type Optional content type (will be determined if missing) * @param string $content_disposition Either attachment (default) or inline * @param Closure $callback Optional callback when download has finished * @param int $chunk_size Optional size of chunks to send (default: 256k) */ public function render_temporary_file( $file, $filename = null, $content_type = null, $content_disposition = 'attachment', Closure $callback = null, $chunk_size = 262144 ) { $delete_callback = function ($file) use ($callback) { unlink($file); if ($callback) { $callback($file); } }; $this->render_file( $file, $filename, $content_type, $content_disposition, $delete_callback, $chunk_size ); } /** * relays current request to another controller and returns the response * the other controller is given all assigned properties, additional parameters are passed * through * * @param string $to_uri a trails route * @return Trails_Response */ public function relay($to_uri/* , ... */) { $args = func_get_args(); $uri = array_shift($args); [$controller_path, $unconsumed] = '' === $uri ? $this->dispatcher->default_route() : $this->dispatcher->parse($uri); $controller = $this->dispatcher->load_controller($controller_path); $assigns = $this->get_assigned_variables(); unset($assigns['controller']); foreach ($assigns as $k => $v) { $controller->$k = $v; } $controller->layout = null; $controller->parent_controller = $this; array_unshift($args, $unconsumed); return $controller->perform_relayed(...$args); } /** * Relays current request and performs redirect if neccessary. * * @param string $to_uri a trails route * @return Trails_Response * * @see StudipController::relay() */ public function relayWithRedirect(...$args): Trails_Response { $response = $this->relay(...$args); // If the relayed action should perform a redirect, do so if (isset($response->headers['Location'])) { header("Location: {$response->headers['Location']}"); page_close(); die; } return $response; } /** * perform a given action/parameter string from an relayed request * before_filter and after_filter methods are not called * * @see perform * @param string $unconsumed * @return Trails_Response */ public function perform_relayed($unconsumed/* , ... */) { $args = func_get_args(); $unconsumed = array_shift($args); [$action, $extracted_args, $format] = $this->extract_action_and_args($unconsumed); $this->format = isset($format) ? $format : 'html'; $this->current_action = $action; $args = array_merge($extracted_args, $args); $callable = $this->map_action($action); if (is_callable($callable)) { $callable(...$args); } else { $this->does_not_understand($action, $args); } if (!$this->performed) { $this->render_action($action); } return $this->response; } /** * Renders a given template and returns the resulting string. * * @param string $template Name of the template file * @param mixed $layout Optional layout * @return string */ public function render_template_as_string($template, $layout = null) { $template = $this->get_template_factory()->open($template); $template->set_layout($layout); $template->set_attributes($this->get_assigned_variables()); return $template->render(); } /** * Magic methods that intercepts all unknown method calls. * If a method is called that matches an action on this controller, * an url to that action is generated. * * Basically, this: * * $controller->url_for('foo/bar/baz/' . $param) * * is equal to calling this on the Foo_BarController: * * $controller->baz($param) * * @param String $method Called method name * @param array $argumetns Provided arguments * @return url to the requested action * @throws Trails_UnknownAction if no action matches the method */ public function __call($method, $arguments) { $function = 'action_link'; if (mb_strpos($method, 'Link') === mb_strlen($method) - 4) { $method = mb_substr($method, 0, -4); } elseif (mb_strpos($method, 'URL') === mb_strlen($method) - 3) { $function = 'action_url'; $method = mb_substr($method, 0, -3); } if (!$this->has_action($method)) { throw new Trails_UnknownAction("Unknown action '{$method}'"); } array_unshift($arguments, $method); return call_user_func_array([$this, $function], $arguments); } /** * Returns whether this controller has the specificed action. * * @param string $action Name of the action * @return true if action is defined, false otherwise */ public function has_action($action) { return method_exists($this, $action . '_action') || ($this->parent_controller && $this->parent_controller->has_action($action)); } /** * Generates the url for an action on this controller without the * neccessity to provide the full "path" to the action (since it * is implicitely known). * * Basically, this: * * $controller->url_for('foo/bar/baz/' . $param) * * is equal to calling this on the Foo_BarController: * * $controller->action_url('baz/' . $param) * * @param string $action Name of the action * @return string url to the requested action */ public function action_url($action) { $arguments = func_get_args(); $arguments[0] = $this->controller_path() . '/' . $arguments[0]; return $this->url_for(...$arguments); } /** * Generates the link for an action on this controller without the * neccessity to provide the full "path" to the action (since it * is implicitely known). * * Basically, this: * * $controller->link_for('foo/bar/baz/' . $param) * * is equal to calling this on the Foo_BarController: * * $controller->action_link('baz/' . $param) * * @param string $action Name of the action * @return string to the requested action */ public function action_link($action) { return htmlReady($this->action_url(...func_get_args())); } /** * Returns the url path to this controller. * * @return string url path to this controller */ protected function controller_path() { $class = get_class($this->parent_controller ?? $this); $controller = mb_substr($class, 0, -mb_strlen('Controller')); $controller = strtosnakecase($controller); return preg_replace('/_{2,}/', '/', $controller); } /** * Validate the datetime according to specific format. * * @param string $datetime the datetime which should be validate * @param string $format the format that the datetime should have by default H:i for time * * @return bool result of validation */ public function validate_datetime($datetime, $format = 'H:i') { $dt = DateTime::createFromFormat($format, $datetime); return $dt && $dt->format($format) == date('H:i',strtotime($datetime)); } /** * Creates the body element id for this controller a given action. * * @param string $unconsumed_path Unconsumed path to extract action from * @return string */ protected function getBodyElementIdForControllerAndAction($unconsumed_path) { // Extract action from unconsumed path segment [$action] = $this->extract_action_and_args($unconsumed_path); // Extract controller name from class name $controller = preg_replace('/Controller$/', '', get_class($this)); $controller = Trails_Inflector::underscore($controller); // Build main parts of the body element id $body_id_parts = explode('/', $controller); $body_id_parts[] = $action; // Create and set body element id $body_id = implode('-', $body_id_parts); return $body_id; } }