<?php class Oer_EndpointsController extends StudipController { protected $with_session = true; //we do need to have a session for this controller public function before_filter(&$action, &$args) { parent::before_filter($action, $args); if ( !Config::get()->OERCAMPUS_ENABLED || ( Config::get()->OER_PUBLIC_STATUS !== 'nobody' && !$GLOBALS['perm']->have_perm(Config::get()->OER_PUBLIC_STATUS) ) ) { throw new AccessDeniedException(); } } public function index_action() { $this->controllerreflection = new ReflectionClass($this); } /** * Returns the public key and some other information of this host. * The returned text is a json-object like * [code] * { * "name": "name of this host", * "public_key": "the armored public key", * "url": "the preferred URL of this host. May be configured in config_local.inc.php as the variable $GLOBALS['OER_PREFERRED_URI'] ", * "index_server": 0 // or 1, 1 if this host is ready to be asked as an index-server, else 0. * } * [/code] */ public function fetch_public_host_key_action() { $host = OERHost::thisOne(); if (Request::get("from")) { $this->refreshHost(Request::get("from")); } $this->render_json([ 'name' => Config::get()->UNI_NAME_CLEAN, 'public_key' => $host['public_key'], 'url' => ($GLOBALS['OER_PREFERRED_URI'] ?? $GLOBALS['ABSOLUTE_URI_STUDIP']) . 'dispatch.php/oer/endpoints/', 'index_server' => $host['index_server'] ]); } /** * Called by a remote-server to update its server-information via post-request. * Even the public key could be updated this way! */ public function update_server_info_action() { if (!Request::isPost()) { throw new Exception("USE POST TO PUSH."); } $host = $this->getHostFromRequest(); if ($host && !$host->isMe()) { $data = $this->extractDataForHost($host); $host['name'] = $data['data']['name']; $host['index_server'] = $data['data']['index_server']; $host['public_key'] = $data['data']['public_key']; $host['url'] = $data['data']['url']; $host['last_updated'] = time(); $host->store(); $this->render_text('stored'); } else { $this->render_text(''); } } /** * Returns a json with all known hosts. * If there is a "from" GET-parameter, this host will * fetch the public key of the from-host and saves it to its database. */ public function fetch_known_hosts_action() { $output = []; if (Request::get("from")) { $this->refreshHost(Request::get("from")); } foreach (OERHost::findAll() as $host) { if (!$host->isMe() && $host['active']) { $output['hosts'][] = [ 'name' => $host['name'], 'url' => $host['url'] ]; } } $this->render_json($output); } protected function refreshHost($url) { $host = OERHost::findOneByUrl($url); if (!$host) { return; } $url = $url."fetch_public_host_key"; $host_data = @file_get_contents($url, false, get_default_http_stream_context($url)); if ($host_data) { $host_data = json_decode($host_data, true); if ($host_data) { $host['name'] = $host_data['name']; $host['public_key'] = $host_data['public_key']; $host['last_updated'] = time(); $host->store(); } } } public function search_items_action() { $host = OERHost::thisOne(); if (Request::get("text")) { $this->materialien = OERMaterial::findByText(Request::get("text")); } elseif (Request::get("tag")) { $this->materialien = OERMaterial::findByTag(Request::get("tag")); } $output = ['results' => []]; if (isset($this->materialien)) { foreach ($this->materialien as $material) { $data = []; $data['host'] = [ 'name' => $material->host ? $material->host['name'] : $host['name'], 'url' => $material->host ? $material->host['url'] : $host['url'], 'public_key' => $material->host ? $material->host['public_key'] : $host['public_key'] ]; $data['data'] = $material->toArray(); unset($data['data']['material_id']); unset($data['data']['uri_hash']); $data['users'] = []; foreach ($material->users as $userdata) { $user = $userdata['external_contact'] ? ExternalUser::find($userdata['user_id']) : User::find($userdata['user_id']); $data['users'][] = [ 'user_id' => $userdata['external_contact'] ? $user->foreign_id : $userdata['user_id'], 'name' => $userdata['external_contact'] ? $user['name'] : get_fullname($userdata['user_id']), 'avatar' => $userdata['external_contact'] ? $user->avatar_url : Avatar::getAvatar($userdata['user_id'])->getURL(Avatar::NORMAL), 'host_url' => $material->host ? $material->host['url'] : $host['url'] ]; } $data['topics'] = []; foreach ($material->getTopics() as $topic) { $data['topics'][] = $topic['name']; } $output['results'][] = $data; } } $this->render_json($output); } /** * Returns data of a given item including where to download it and the structure, decription, etc. * If item is not hosted on this server, just relocate the request to the real server. * * This endpoint should be called by a remote whenever a client wants to view the details of an item. * * @param $item_id : ID of the item on this server. */ public function get_item_data_action($item_id) { $material = new OERMaterial($item_id); if ($material->isNew()) { $this->render_json([ 'deleted' => 1 ]); } elseif (!$material['foreign_material_id']) { $me = OERHost::thisOne(); $topics = []; foreach ($material->getTopics() as $topic) { $topics[] = $topic['name']; } $reviews = []; foreach ($material->reviews as $review) { if ($review['metadata']['host_id']) { $user = ExternalUser::find($review['user_id']); $user = [ 'user_id' => $review['user_id'], 'name' => $user['name'], 'avatar' => $user['avatar_url'], 'description' => $user['data']['description'] ]; } else { $user = [ 'user_id' => $review['user_id'], 'name' =>get_fullname($review['user_id']), 'avatar' => Avatar::getAvatar($review['user_id'])->getURL(Avatar::NORMAL), 'description' => $review->user['oercampus_description'] ]; } $reviews[] = [ 'foreign_review_id' => $review['metadata']['foreign_review_id'] ?: $review->getId(), 'review' => $review['content'], 'rating' => $review['metadata']['rating'], 'user' => $user, 'host' => [ 'name' => $review['metadata']['host_id'] ? $review->host['name'] : $me['name'], 'url' => $review['metadata']['host_id'] ? $review->host['url'] : $me['url'], 'public_key' => $review['metadata']['host_id'] ? $review->host['public_key'] : $me['public_key'] ], 'mkdate' => $review['mkdate'], 'chkdate' => $review['chdate'] ]; } $users = []; foreach ($material->users as $userdata) { $user = $userdata['external_contact'] ? ExternalUser::find($userdata['user_id']) : User::find($userdata['user_id']); $users[] = [ 'user_id' => $userdata['external_contact'] ? $user->foreign_id : $userdata['user_id'], 'name' => $userdata['external_contact'] ? $user['name'] : get_fullname($userdata['user_id']), 'avatar' => $userdata['external_contact'] ? $user->avatar_url : Avatar::getAvatar($userdata['user_id'])->getURL(Avatar::NORMAL), 'description' => $userdata['external_contact'] ? $user['data']['description'] : $user->datafields, 'host_url' => $material->host ? $material->host['url'] : $me['url'] ]; } $this->render_json([ 'data' => [ 'name' => $material['name'], 'short_description' => $material['short_description'], 'description' => $material['description'], 'content_type' => $material['content_type'], 'front_image_content_type' => $material['front_image_content_type'], 'url' => ($GLOBALS['OER_PREFERRED_URI'] ?? $GLOBALS['ABSOLUTE_URI_STUDIP']) . 'dispatch.php/oer/market/download/' . $item_id, 'player_url' => $material['player_url'], 'tool' => $material['tool'], 'structure' => ($material['structure'] ? $material['structure']->getArrayCopy() : null), 'license' => $material['license'], 'uri' => $material['uri'] ], 'users' => $users, 'topics' => $topics, 'reviews' => $reviews ]); } else { $host = new OERHost($material['host_id']); header("Location: ".$host['url']."get_item_data/".$item_id); return; } } /** * Update data of an item via POST-request. */ public function push_data_action() { if (!Request::isPost()) { throw new Exception("USE POST TO PUSH."); } $host = $this->getHostFromRequest(); if ($host && !$host->isMe()) { $data = $this->extractDataForHost($host); $material = OERMaterial::findOneBySQL("host_id = ? AND foreign_material_id = ?", [ $host->getId(), $data['data']['foreign_material_id'] ]); if (!$material) { $material = new OERMaterial(); } if ($data['delete_material']) { $material->delete(); $this->render_text("deleted "); } else { $material->setData($data['data']); $material['host_id'] = $host->getId(); $material->store(); $material->setTopics($data['topics']); $material->setUsers($data['users']); $this->render_text("stored "); } } else { $this->render_text(''); } } /** * Download an item from this server. The ##material_id## of the item must be given. * @param $material_id : material_id from this server or foreign_material_id from another server. */ public function download_action($material_id, $disposition = "inline") { $this->material = new OERMaterial($material_id); if ($this->material['draft'] && !$this->material->isMine()) { throw new AccessDeniedException(); } while (ob_get_level()) { ob_end_clean(); } page_close(); if (!file_exists($this->material->getFilePath())) { throw new Exception(_('Die gewünschte Datei konnte nicht gefunden werden.')); } $filesize = filesize($this->material->getFilePath()); $content_type = $this->material['content_type']; if (!in_array($content_type, get_mime_types())) { $content_type = 'application/octet-stream'; } if ($content_type === 'application/octet-stream') { $content_disposition = 'attachment'; } else { $content_disposition = $disposition === 'attachment' ? 'attachment' : 'inline'; } header("Accept-Ranges: bytes"); $start = 0; $end = $filesize - 1; $length = $filesize; if (isset($_SERVER['HTTP_RANGE'])) { $c_start = $start; $c_end = $end; [, $range] = explode('=', $_SERVER['HTTP_RANGE'], 2); if (mb_strpos($range, ',') !== false) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $start-$end/$filesize"); exit; } if ($range[0] == '-') { $c_start = $filesize - mb_substr($range, 1); } else { $range = explode('-', $range); $c_start = $range[0]; $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $filesize; } $c_end = ($c_end > $end) ? $end : $c_end; if ($c_start > $c_end || $c_start > $filesize - 1 || $c_end >= $filesize) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $start-$end/$filesize"); exit; } $start = $c_start; $end = $c_end; $length = $end - $start + 1; header('HTTP/1.1 206 Partial Content'); header("Content-Range: bytes $start-$end/$filesize"); } header("Content-Length: $length"); header("Expires: Mon, 12 Dec 2001 08:00:00 GMT"); header("Last-Modified: " . gmdate ("D, d M Y H:i:s") . " GMT"); if ($_SERVER['HTTPS'] == "on") { header("Pragma: public"); header("Cache-Control: private"); } else { header("Pragma: no-cache"); header("Cache-Control: no-store, no-cache, must-revalidate"); // HTTP/1.1 } header("Cache-Control: post-check=0, pre-check=0", false); header("Content-Type: " . $content_type); header("Content-Disposition: " . $content_disposition . "; " . encode_header_parameter('filename', $this->material['filename'])); readfile_chunked($this->material->getFilePath(), $start, $end); if (!$start) { OERDownloadcounter::addCounter($material_id); } die(); } /** * Download image of this item from this server. The ##material_id## of the item must be given. * @param $material_id : material_id from this server or foreign_material_id from another server. */ public function download_front_image_action($material_id) { $this->material = new OERMaterial($material_id); if ( stripos($this->material['front_image_content_type'], 'image') === 0 && stripos($this->material['front_image_content_type'], 'svg') === false ) { $this->set_content_type($this->material['front_image_content_type']); $this->response->add_header('Content-Disposition', 'inline'); $this->response->add_header('Content-Length', filesize($this->material->getFrontImageFilePath())); $this->render_text(file_get_contents($this->material->getFrontImageFilePath())); } else { throw new Trails_Exception(404); } } /** * Adds or edits a review to the material on this server from a client of another server. * Use this request only as a POST request, the body must be a JSON-object that carries all the * necessary variables. * @param $material_id : ID of the item on this server. */ public function add_review_action($material_id) { if (!Request::isPost()) { throw new Exception("USE POST TO PUSH."); } $host = $this->getHostFromRequest(); if ($host && !$host->isMe()) { $data = $this->extractDataForHost($host); $material = new OERMaterial($material_id); if ($material->isNew() || $material['host_id']) { throw new Exception("Unknown material."); } $user = ExternalUser::findOneBySQL("host_id = ? AND foreign_id = ?", [ $host->getId(), $data['user']['user_id'] ]); if (!$user) { $user = new ExternalUser(); $user['host_id'] = $host->getId(); $user['foreign_id'] = $data['user']['user_id']; } $user['contact_type'] = 'oercampus'; $user['name'] = $data['user']['name']; $user['avatar_url'] = $data['user']['avatar']; $user['data'] = [ 'description' => $data['user']['description'] ?: '' ]; $user->store(); $review = OERReview::findOneBySQL("display_class = 'OERReview' AND external_contact = '1' AND context_id = :material_id AND user_id = :user_id AND metadata LIKE :host_id", [ 'material_id' => $material_id, 'user_id' => $user->getId(), 'host_id' => "%".$host->getId()."%" ]); if (!$review) { $review = new OERReview(); $review['user_id'] = $user->getId(); $review['display_class'] = "OERReview"; $review['context_id'] = $material_id; } $review['content'] = $data['data']['review']; $review['metadata'] = [ 'host_id' => $host->getId(), 'foreign_review_id' => $data['data']['foreign_review_id'], 'rating' => $data['data']['rating'] ]; $review['mkdate'] = $data['data']['mkdate']; $review['chdate'] = $data['data']['chdate']; $review->store(); $this->render_text("stored "); } else { $this->render_text(''); } } /** * Adds or edits a comment to the material on this server from a client of another server. * Use this request only as a POST request, the body must be a JSON-object that carries all the * necessary variables. * The review_id is the foreign_review_id if the host_hash is not empty or the review_id if the host_hash is empty. * @param $material_id : ID of the item on this server. */ public function add_comment_action($review_id, $host_hash = null) { if (!Request::isPost()) { throw new Exception("USE POST TO PUSH."); } $host = $this->getHostFromRequest(); if ($host && !$host->isMe()) { if ($host_hash) { $review = OERReview::findOneBySQL(" display_class = 'OERREview' AND context_type = 'public' AND metadata LIKE :foreign_review_id AND metadata LIKE :host_id ", [ 'foreign_review_id' => "%".$review_id."%", 'host_id' => "%".$host->getId()."%" ]); } else { $review = OERReview::find($review_id); } if (!$review) { throw new Exception("Unknown material."); } $data = $this->extractDataForHost($host); $user = ExternalUser::findOneBySQL("host_id = ? AND foreign_id = ?", [ $host->getId(), $data['user']['user_id'] ]); if (!$user) { $user = new ExternalUser(); $user['host_id'] = $host->getId(); $user['foreign_id'] = $data['user']['user_id']; } $user['contact_type'] = 'oercampus'; $user['name'] = $data['user']['name']; $user['avatar_url'] = $data['user']['avatar']; $user['data'] = [ 'description' => $data['user']['description'] ?: '' ]; $user->store(); $comment = new BlubberComment(); $comment['user_id'] = $user->getId(); $comment['external_contact'] = "1"; $comment['thread_id'] = $review->getId(); $comment['content'] = $data['data']['comment']; $comment['mkdate'] = $data['data']['mkdate']; $comment['chdate'] = $data['data']['chdate']; $comment->store(); $this->render_text("stored "); } else { $this->render_text(''); } } private function getHostFromRequest() { $public_key_hash = $this->getHttpHeader(OERHost::OER_HEADER_PUBLIC_KEY_HASH); //MD5_HASH_OF_RSA_PUBLIC_KEY return OERHost::findOneBySQL("MD5(public_key) = ?", [$public_key_hash]); } private function extractDataForHost(OERHost $host) { $encoded_signature = $this->getHttpHeader(OERHost::OER_HEADER_SIGNATURE); //BASE64_RSA_SIGNATURE $signature = base64_decode($encoded_signature); $body = file_get_contents('php://input'); if (!$host->verifySignature($body, $signature)) { throw new Exception('Wrong signature, sorry.'); } return json_decode($body, true); } private function getHttpHeader($name) { $header_name = 'HTTP_' . str_replace('-', '_', mb_strtoupper($name)); return $_SERVER[$header_name]; } }