new \Slim\Views\Twig(), 'mode' => 'production', #'mode' => 'debug', #'mode' => 'development', )); $app->configureMode('production', 'confprod'); $app->configureMode('development', 'confdev'); $app->configureMode('debug', 'confdebug'); /** * Configure app for production */ function confprod() { global $app, $appname, $appversion; $app->config(array( 'debug' => false, 'cookies.lifetime' => '1 day', 'cookies.secret_key' => 'b4924c3579e2850a6fad8597da7ad24bf43ab78e', )); $app->getLog()->setEnabled(true); $app->getLog()->setLevel(\Slim\Log::WARN); $app->getLog()->info($appname . ' ' . $appversion . ': Running in production mode.'); } /** * Configure app for development */ function confdev() { global $app, $appname, $appversion; $app->config(array( 'debug' => true, 'cookies.lifetime' => '5 minutes', 'cookies.secret_key' => 'b4924c3579e2850a6fad8597da7ad24bf43ab78e', )); $app->getLog()->setEnabled(true); $app->getLog()->setLevel(\Slim\Log::DEBUG); $app->getLog()->info($appname . ' ' . $appversion . ': Running in development mode.'); } /** * Configure app for debug mode: production + log everything to file */ function confdebug() { global $app, $appname, $appversion; $app->config(array( 'debug' => true, 'cookies.lifetime' => '1 day', 'cookies.secret_key' => 'b4924c3579e2850a6fad8597da7ad24bf43ab78e', )); require 'vendor/DateTimeFileWriter.php'; $app->getLog()->setEnabled(true); $app->getLog()->setLevel(\Slim\Log::DEBUG); $app->getLog()->setWriter(new \Slim\Extras\Log\DateTimeFileWriter(array('path' => './data', 'name_format' => 'Y-m-d'))); $app->getLog()->info($appname . ' ' . $appversion . ': Running in debug mode.'); error_reporting(E_ALL); } # Init app globals $globalSettings = array(); $globalSettings['appname'] = $appname; $globalSettings['version'] = $appversion; $globalSettings['sep'] = ' :: '; # Find the user language, either one of the allowed languages or # English as a fallback. $globalSettings['lang'] = getUserLang($allowedLangs, $fallbackLang); $globalSettings['l10n'] = new L10n($globalSettings['lang']); $globalSettings['langa'] = $globalSettings['l10n']->langa; $globalSettings['langb'] = $globalSettings['l10n']->langb; # Init admin settings with std values, for upgrades or db errors $globalSettings[CALIBRE_DIR] = ''; $globalSettings[DB_VERSION] = DB_SCHEMA_VERSION; $globalSettings[KINDLE] = 0; $globalSettings[KINDLE_FROM_EMAIL] = ''; $globalSettings[THUMB_GEN_CLIPPED] = 1; $globalSettings[PAGE_SIZE] = 30; $globalSettings[DISPLAY_APP_NAME] = $appname; $globalSettings[MAILER] = Mailer::MAIL; $globalSettings[SMTP_USER] = ''; $globalSettings[SMTP_PASSWORD] = ''; $globalSettings[SMTP_SERVER] = ''; $globalSettings[SMTP_PORT] = 25; $globalSettings[SMTP_ENCRYPTION] = 0; $globalSettings[METADATA_UPDATE] = 0; $globalSettings[LOGIN_REQUIRED] = 1; $globalSettings[TITLE_TIME_SORT] = TITLE_TIME_SORT_TIMESTAMP; $knownConfigs = array(CALIBRE_DIR, DB_VERSION, KINDLE, KINDLE_FROM_EMAIL, THUMB_GEN_CLIPPED, PAGE_SIZE, DISPLAY_APP_NAME, MAILER, SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_ENCRYPTION, METADATA_UPDATE, LOGIN_REQUIRED, TITLE_TIME_SORT); # Freeze (true) DB schema before release! Set to false for DB development. $app->bbs = new BicBucStriim('data/data.db', true); $app->add(new \CalibreConfigMiddleware(CALIBRE_DIR)); $app->add(new \LoginMiddleware($appname, array('js', 'img', 'style'))); $app->add(new \OwnConfigMiddleware($knownConfigs)); $app->add(new \CachingMiddleware(array('/admin', '/login'))); ###### Init routes for production $app->notFound('myNotFound'); $app->get('/', 'main'); $app->get('/admin/', 'check_admin', 'admin'); $app->get('/admin/configuration/', 'check_admin', 'admin_configuration'); $app->post('/admin/configuration/', 'check_admin', 'admin_change_json'); $app->get('/admin/idtemplates/', 'check_admin', 'admin_get_idtemplates'); $app->put('/admin/idtemplates/:id/', 'check_admin', 'admin_modify_idtemplate'); $app->delete('/admin/idtemplates/:id/', 'check_admin', 'admin_clear_idtemplate'); $app->get('/admin/mail/', 'check_admin', 'admin_get_smtp_config'); $app->put('/admin/mail/', 'check_admin', 'admin_change_smtp_config'); $app->get('/admin/users/', 'check_admin', 'admin_get_users'); $app->post('/admin/users/', 'check_admin', 'admin_add_user'); $app->get('/admin/users/:id/', 'check_admin', 'admin_get_user'); $app->put('/admin/users/:id/', 'check_admin', 'admin_modify_user'); $app->delete('/admin/users/:id/', 'check_admin', 'admin_delete_user'); $app->get('/admin/version/', 'check_admin', 'admin_check_version'); $app->get('/authors/:id/notes/', 'check_admin', 'authorNotes'); #$app->post('/authors/:id/notes/', 'check_admin', 'authorNotesEdit'); $app->get('/authors/:id/:page/', 'authorDetailsSlice'); $app->get('/authorslist/:id/', 'authorsSlice'); $app->get('/login/', 'show_login'); $app->post('/login/', 'perform_login'); $app->get('/logout/', 'logout'); $app->post('/metadata/authors/:id/thumbnail/', 'check_admin', 'edit_author_thm'); $app->delete('/metadata/authors/:id/thumbnail/', 'check_admin', 'del_author_thm'); $app->post('/metadata/authors/:id/notes/', 'check_admin', 'edit_author_notes'); $app->delete('/metadata/authors/:id/notes/', 'check_admin', 'del_author_notes'); $app->post('/metadata/authors/:id/links/', 'check_admin', 'new_author_link'); $app->delete('/metadata/authors/:id/links/:link_id/', 'check_admin', 'del_author_link'); $app->get('/search/', 'globalSearch'); $app->get('/series/:id/:page/', 'seriesDetailsSlice'); $app->get('/serieslist/:id/', 'seriesSlice'); $app->get('/tags/:id/:page/', 'tagDetailsSlice'); $app->get('/tagslist/:id/', 'tagsSlice'); $app->get('/titles/:id/', 'title'); $app->get('/titles/:id/cover/', 'cover'); $app->get('/titles/:id/file/:file', 'book'); $app->post('/titles/:id/kindle/:file', 'kindle'); $app->get('/titles/:id/thumbnail/', 'thumbnail'); $app->get('/titleslist/:id/', 'titlesSlice'); $app->get('/opds/', 'opdsRoot'); $app->get('/opds/newest/', 'opdsNewest'); $app->get('/opds/titleslist/:id/', 'opdsByTitle'); $app->get('/opds/authorslist/', 'opdsByAuthorInitial'); $app->get('/opds/authorslist/:initial/', 'opdsByAuthorNamesForInitial'); $app->get('/opds/authorslist/:initial/:id/:page/', 'opdsByAuthor'); $app->get('/opds/tagslist/', 'opdsByTagInitial'); $app->get('/opds/tagslist/:initial/', 'opdsByTagNamesForInitial'); $app->get('/opds/tagslist/:initial/:id/:page/', 'opdsByTag'); $app->get('/opds/serieslist/', 'opdsBySeriesInitial'); $app->get('/opds/serieslist/:initial/', 'opdsBySeriesNamesForInitial'); $app->get('/opds/serieslist/:initial/:id/:page/', 'opdsBySeries'); $app->get('/opds/opensearch.xml', 'opdsSearchDescriptor'); $app->get('/opds/searchlist/:id/', 'opdsBySearch'); $app->get('/opds/titles/:id/', 'title'); $app->get('/opds/titles/:id/cover/', 'cover'); $app->get('/opds/titles/:id/file/:file', 'book'); $app->get('/opds/titles/:id/thumbnail/', 'thumbnail'); $app->run(); /********************************************************************* * Production functions ********************************************************************/ /** * 404 page for invalid URLs */ function myNotFound() { global $app, $globalSettings; $app->render('error.html', array( 'page' => mkPage(getMessageString('not_found1')), 'title' => getMessageString('not_found1'), 'error' => getMessageString('not_found2'))); } function show_login() { global $app; $app->render('login.html', array( 'page' => mkPage(getMessageString('login')))); } function perform_login() { global $app; $login_data = $app->request()->post(); $app->getLog()->debug('login: ' . var_export($login_data, true)); if (isset($login_data['username']) && isset($login_data['password'])) { $uname = $login_data['username']; $upw = $login_data['password']; if (empty($uname) || empty($upw)) { $app->render('login.html', array( 'page' => mkPage(getMessageString('login')))); } else { $app->login_service->login($app->auth, array('username' => $uname, 'password' => $upw)); $success = $app->auth->getStatus(); $app->getLog()->debug('login success: ' . $success); if (is_authenticated()) { $app->getLog()->info('logged in user : ' . $app->auth->getUserName()); $app->redirect($app->request->getRootUri() . '/'); } else { $app->getLog()->error('error logging in user : ' . $login_data['username']); $app->render('login.html', array( 'page' => mkPage(getMessageString('login')))); } } } else { $app->render('login.html', array( 'page' => mkPage(getMessageString('login')))); } } function logout() { global $app; if (is_authenticated()) { $username = $app->auth->getUserName(); $app->getLog()->debug("logging out user: " . $username); $app->logout_service->logout($app->auth); if (is_authenticated()) { $app->getLog()->error("error logging out user: " . $username); } else { $app->getLog()->info("logged out user: " . $username); } } $app->render('logout.html', array( 'page' => mkPage(getMessageString('logout')))); } /** * Check admin rights and redirect if necessary */ function check_admin() { global $app; if (!is_admin()) { $app->render('error.html', array( 'page' => mkPage(getMessageString('error'), 0, 0), 'error' => getMessageString('error_no_access'))); } } /** * Generate the admin page -> /admin/ */ function admin() { global $app; $app->render('admin.html', array( 'page' => mkPage(getMessageString('admin'), 0, 1), 'isadmin' => is_admin())); } function mkMailers() { $e0 = new ConfigMailer(); $e0->key = Mailer::SMTP; $e0->text = getMessageString('admin_mailer_smtp'); $e1 = new ConfigMailer(); $e1->key = Mailer::SENDMAIL; $e1->text = getMessageString('admin_mailer_sendmail'); $e2 = new ConfigMailer(); $e2->key = Mailer::MAIL; $e2->text = getMessageString('admin_mailer_mail'); return array($e0, $e1, $e2); } function mkTitleTimeSortOptions() { $e0 = new ConfigTtsOption(); $e0->key = TITLE_TIME_SORT_TIMESTAMP; $e0->text = getMessageString('admin_tts_by_timestamp'); $e1 = new ConfigTtsOption(); $e1->key = TITLE_TIME_SORT_PUBDATE; $e1->text = getMessageString('admin_tts_by_pubdate'); $e2 = new ConfigTtsOption(); $e2->key = TITLE_TIME_SORT_LASTMODIFIED; $e2->text = getMessageString('admin_tts_by_lastmodified'); return array($e0, $e1, $e2); } /** * Generate the configuration page -> GET /admin/configuration/ */ function admin_configuration() { global $app; $app->render('admin_configuration.html', array( 'page' => mkPage(getMessageString('admin'), 0, 2), 'mailers' => mkMailers(), 'ttss' => mkTitleTimeSortOptions(), 'isadmin' => is_admin())); } /** * Generate the ID templates page -> GET /admin/idtemplates/ */ function admin_get_idtemplates() { global $app; $idtemplates = $app->bbs->idTemplates(); $idtypes = $app->calibre->idTypes(); $ids2add = array(); foreach ($idtypes as $idtype) { if (empty($idtemplates)) { array_push($ids2add, $idtype['type']); } else { $found = false; foreach ($idtemplates as $idtemplate) { if ($idtype['type'] === $idtemplate->name) { $found = true; break; } } if (!$found) array_push($ids2add, $idtype['type']); } } foreach ($ids2add as $id2add) { $ni = new IdUrlTemplate(); $ni->name = $id2add; $ni->val = ''; $ni->label = ''; array_push($idtemplates, $ni); } $app->getLog()->debug('admin_get_idtemplates ' . var_export($idtemplates, true)); $app->render('admin_idtemplates.html', array( 'page' => mkPage(getMessageString('admin_idtemplates'), 0, 2), 'templates' => $idtemplates, 'isadmin' => is_admin())); } function admin_modify_idtemplate($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('admin_modify_idtemplate: invalid template id ' . $id); $app->halt(400, "Bad parameter"); } $template_data = $app->request()->put(); $app->getLog()->debug('admin_modify_idtemplate: ' . var_export($template_data, true)); try { $template = $app->bbs->idTemplate($id); if (is_null($template)) $ntemplate = $app->bbs->addIdTemplate($id, $template_data['url'], $template_data['label']); else $ntemplate = $app->bbs->changeIdTemplate($id, $template_data['url'], $template_data['label']); } catch (Exception $e) { $app->getLog()->error('admin_modify_idtemplate: error while adding template' . var_export($template_data, true)); $app->getLog()->error('admin_modify_idtemplate: exception ' . $e->getMessage()); $ntemplate = null; } $resp = $app->response(); if (!is_null($ntemplate)) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('template' => $ntemplate->getProperties(), 'msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } #$app->getLog()->debug('admin_modify_idtemplate 2: '.var_export($ntemplate, true)); $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } function admin_clear_idtemplate($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('admin_clear_idtemplate: invalid template id ' . $id); $app->halt(400, "Bad parameter"); } $app->getLog()->debug('admin_clear_idtemplate: ' . var_export($id, true)); $success = $app->bbs->deleteIdTemplate($id); $resp = $app->response(); if ($success) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(404); $answer = getMessageString('admin_modify_error'); $resp->header('Content-type', 'text/plain'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Generate the SMTP configuration page -> GET /admin/mail/ */ function admin_get_smtp_config() { global $app, $globalSettings; $mail = array('username' => $globalSettings[SMTP_USER], 'password' => $globalSettings[SMTP_PASSWORD], 'smtpserver' => $globalSettings[SMTP_SERVER], 'smtpport' => $globalSettings[SMTP_PORT], 'smtpenc' => $globalSettings[SMTP_ENCRYPTION]); $app->render('admin_mail.html', array( 'page' => mkPage(getMessageString('admin_mail'), 0, 2), 'mail' => $mail, 'encryptions' => mkEncryptions(), 'isadmin' => is_admin())); } function mkEncryptions() { $e0 = new Encryption(); $e0->key = 0; $e0->text = getMessageString('admin_smtpenc_none'); $e1 = new Encryption(); $e1->key = 1; $e1->text = getMessageString('admin_smtpenc_ssl'); $e2 = new Encryption(); $e2->key = 2; $e2->text = getMessageString('admin_smtpenc_tls'); return array($e0, $e1, $e2); } /** * Change the SMTP configuration -> PUT /admin/mail/ */ function admin_change_smtp_config() { global $app; $mail_data = $app->request()->put(); $app->getLog()->debug('admin_change_smtp_configuration: ' . var_export($mail_data, true)); $mail_config = array(SMTP_USER => $mail_data['username'], SMTP_PASSWORD => $mail_data['password'], SMTP_SERVER => $mail_data['smtpserver'], SMTP_PORT => $mail_data['smtpport'], SMTP_ENCRYPTION => $mail_data['smtpenc']); $app->bbs->saveConfigs($mail_config); $resp = $app->response(); $app->render('admin_mail.html', array( 'page' => mkPage(getMessageString('admin_smtp'), 0, 2), 'mail' => $mail_data, 'encryptions' => mkEncryptions(), 'isadmin' => is_admin())); } /** * Generate the users overview page -> GET /admin/users/ */ function admin_get_users() { global $app; $users = $app->bbs->users(); $app->render('admin_users.html', array( 'page' => mkPage(getMessageString('admin_users'), 0, 2), 'users' => $users, 'isadmin' => is_admin())); } /** * Generate the single user page -> GET /admin/users/:id/ */ function admin_get_user($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('admin_get_user: invalid user id ' . $id); $app->halt(400, "Bad parameter"); } $user = $app->bbs->user($id); $languages = $app->calibre->languages(); foreach ($languages as $language) { $language->key = $language->lang_code; } $nl = new Language(); $nl->lang_code = getMessageString('admin_no_selection'); $nl->key = ''; array_unshift($languages, $nl); $tags = $app->calibre->tags(); foreach ($tags as $tag) { $tag->key = $tag->name; } $nt = new Tag(); $nt->name = getMessageString('admin_no_selection'); $nt->key = ''; array_unshift($tags, $nt); $app->getLog()->debug('admin_get_user: ' . var_export($user, true)); $app->render('admin_user.html', array( 'page' => mkPage(getMessageString('admin_users'), 0, 3), 'user' => $user, 'languages' => $languages, 'tags' => $tags, 'isadmin' => is_admin())); } /** * Add a user -> POST /admin/users/ (JSON) */ function admin_add_user() { global $app; $user_data = $app->request()->post(); $app->getLog()->debug('admin_add_user: ' . var_export($user_data, true)); try { $user = $app->bbs->addUser($user_data['username'], $user_data['password']); } catch (Exception $e) { $app->getLog()->error('admin_add_user: error for adding user ' . var_export($user_data, true)); $app->getLog()->error('admin_add_user: exception ' . $e->getMessage()); $user = null; } $resp = $app->response(); if (isset($user) && !is_null($user)) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('user' => $user->getProperties(), 'msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Delete a user -> DELETE /admin/users/:id/ (JSON) */ function admin_delete_user($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('admin_delete_user: invalid user id ' . $id); $app->halt(400, "Bad parameter"); } $app->getLog()->debug('admin_delete_user: ' . var_export($id, true)); $success = $app->bbs->deleteUser($id); $resp = $app->response(); if ($success) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Modify a user -> PUT /admin/users/:id/ (JSON) */ function admin_modify_user($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('admin_modify_user: invalid user id ' . $id); $app->halt(400, "Bad parameter"); } $user_data = $app->request()->put(); $app->getLog()->debug('admin_modify_user: ' . var_export($user_data, true)); $user = $app->bbs->changeUser($id, $user_data['password'], $user_data['languages'], $user_data['tags'], $user_data['role']); $app->getLog()->debug('admin_modify_user: ' . var_export($user, true)); $resp = $app->response(); if (isset($user) && !is_null($user)) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('user' => $user->getProperties(), 'msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Processes changes in the admin page -> POST /admin/configuration/ */ function admin_change_json() { global $app, $globalSettings; $app->getLog()->debug('admin_change: started'); # Check access permission if (!is_admin()) { $app->getLog()->warn('admin_change: no admin permission'); $app->render('admin_configuration.html', array( 'page' => mkPage(getMessageString('admin')), 'messages' => array(getMessageString('invalid_password')), 'isadmin' => false)); return; } $nconfigs = array(); $req_configs = $app->request()->post(); $errors = array(); $messages = array(); $app->getLog()->debug('admin_change: ' . var_export($req_configs, true)); ## Check for consistency - calibre directory # Calibre dir is still empty and no change in sight --> error if (!has_valid_calibre_dir() && empty($req_configs[CALIBRE_DIR])) array_push($errors, 1); # Calibre dir changed, check it for existence, delete thumbnails of old calibre library elseif (array_key_exists(CALIBRE_DIR, $req_configs)) { $req_calibre_dir = $req_configs[CALIBRE_DIR]; if ($req_calibre_dir != $globalSettings[CALIBRE_DIR]) { if (!Calibre::checkForCalibre($req_calibre_dir)) { array_push($errors, 1); } elseif ($app->bbs->clearThumbnails()) $app->getLog()->info('admin_change: Lib changed, deleted exisiting thumbnails.'); else { $app->getLog()->info('admin_change: Lib changed, deletion of exisiting thumbnails failed.'); } } } ## More consistency checks - kindle feature # Switch off Kindle feature, if no valid email address supplied if ($req_configs[KINDLE] == "1") { if (empty($req_configs[KINDLE_FROM_EMAIL])) { array_push($errors, 5); } elseif (!isEMailValid($req_configs[KINDLE_FROM_EMAIL])) { array_push($errors, 5); } } ## Check for a change in the thumbnail generation method if ($req_configs[THUMB_GEN_CLIPPED] != $globalSettings[THUMB_GEN_CLIPPED]) { $app->getLog()->info('admin_change: Thumbnail generation method changed. Exisiting Thumbnails will be deleted.'); # Delete old thumbnails if necessary if ($app->bbs->clearThumbnails()) $app->getLog()->info('admin_change: Deleted exisiting thumbnails.'); else { $app->getLog()->info('admin_change: Deletion of exisiting thumbnails failed.'); } } ## Check for a change in page size, min 1, max 100 if ($req_configs[PAGE_SIZE] != $globalSettings[PAGE_SIZE]) { if ($req_configs[PAGE_SIZE] < 1 || $req_configs[PAGE_SIZE] > 100) { $app->getLog()->warn('admin_change: Invalid page size requested: ' . $req_configs[PAGE_SIZE]); array_push($errors, 4); } } # Don't save just return the error status if (count($errors) > 0) { $app->getLog()->error('admin_change: ended with error ' . var_export($errors, true)); $app->render('admin_configuration.html', array( 'page' => mkPage(getMessageString('admin')), 'isadmin' => true, 'errors' => $errors)); } else { ## Apply changes foreach ($req_configs as $key => $value) { if (!isset($globalSettings[$key]) || $value != $globalSettings[$key]) { $nconfigs[$key] = $value; $globalSettings[$key] = $value; $app->getLog()->debug('admin_change: ' . $key . ' changed: ' . $value); } } # Save changes if (count($nconfigs) > 0) { $app->bbs->saveConfigs($nconfigs); $app->getLog()->debug('admin_change: changes saved'); } $app->getLog()->debug('admin_change: ended'); $app->render('admin_configuration.html', array( 'page' => mkPage(getMessageString('admin'), 0, 2), 'messages' => array(getMessageString('changes_saved')), 'mailers' => mkMailers(), 'ttss' => mkTitleTimeSortOptions(), 'isadmin' => true, )); } } /** * Get the new version info and compare it to our version -> GET /admin/version/ */ function admin_check_version() { global $app, $globalSettings; $versionAnswer = array(); $contents = file_get_contents(VERSION_URL); if ($contents == false) { $versionClass = 'error'; $versionAnswer = sprintf(getMessageString('admin_new_version_error'), $globalSettings['version']); } else { $versionInfo = json_decode($contents); $version = $globalSettings['version']; if (strpos($globalSettings['version'], '-') === false) { $v = preg_split('/-/', $globalSettings['version']); $version = $v[0]; } $result = version_compare($version, $versionInfo->{'version'}); if ($result === -1) { $versionClass = 'success'; $msg1 = sprintf(getMessageString('admin_new_version'), $versionInfo->{'version'}, $globalSettings['version']); $msg2 = sprintf("%s", $versionInfo->{'url'}, $versionInfo->{'url'}); $msg3 = sprintf(getMessageString('admin_check_url'), $msg2); $versionAnswer = $msg1 . '. ' . $msg3; } else { $versionClass = ''; $versionAnswer = sprintf(getMessageString('admin_no_new_version'), $globalSettings['version']); } } $app->render('admin_version.html', array( 'page' => mkPage(getMessageString('admin_check_version'), 0, 2), 'versionClass' => $versionClass, 'versionAnswer' => $versionAnswer, 'isadmin' => true, )); } /********************************************************************* * Metadata functions ********************************************************************/ /** * Upload an author thumbnail picture -> POST /metadata/authors/:id/thumbnail/ * Works only with JPG/PNG, max. size 3MB */ function edit_author_thm($id) { global $app, $globalSettings; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('edit_author_thm: invalid author id ' . $id); $app->halt(400, "Bad parameter"); } $allowedExts = array("jpeg", "jpg", "png"); #$temp = explode(".", $_FILES["file"]["name"]); #$extension = end($temp); $extension = pathinfo($_FILES["file"]["name"], PATHINFO_EXTENSION); $app->getLog()->debug('edit_author_thm: ' . $_FILES["file"]["name"]); if ((($_FILES["file"]["type"] == "image/jpeg") || ($_FILES["file"]["type"] == "image/jpg") || ($_FILES["file"]["type"] == "image/pjpeg") || ($_FILES["file"]["type"] == "image/x-png") || ($_FILES["file"]["type"] == "image/png")) && ($_FILES["file"]["size"] < 3145728) && in_array($extension, $allowedExts) ) { $app->getLog()->debug('edit_author_thm: filetype ' . $_FILES["file"]["type"] . ', size ' . $_FILES["file"]["size"]); if ($_FILES["file"]["error"] > 0) { $app->getLog()->debug('edit_author_thm: upload error ' . $_FILES["file"]["error"]); $app->flash('error', getMessageString('author_thumbnail_upload_error1') . ': ' . $_FILES["file"]["error"]); $rot = $app->request()->getRootUri(); $app->redirect($rot . '/authors/' . $id . '/0/'); } else { $app->getLog()->debug('edit_author_thm: upload ok, converting'); $author = $app->calibre->author($id); $created = $app->bbs->editAuthorThumbnail($id, $author->name, $globalSettings[THUMB_GEN_CLIPPED], $_FILES["file"]["tmp_name"], $_FILES["file"]["type"]); $app->getLog()->debug('edit_author_thm: converted, redirecting'); $rot = $app->request()->getRootUri(); $app->redirect($rot . '/authors/' . $id . '/0/'); } } else { $app->getLog()->warn('edit_author_thm: Uploaded thumbnail too big or wrong type'); $app->flash('error', getMessageString('author_thumbnail_upload_error2')); $rot = $app->request()->getRootUri(); $app->redirect($rot . '/authors/' . $id . '/0/'); } } /** * Delete the author's thumbnail -> DELETE /metadata/authors/:id/thumbnail/ JSON */ function del_author_thm($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('del_author_thm: invalid author id ' . $id); $app->halt(400, "Bad parameter"); } $app->getLog()->debug('del_author_thm: ' . $id); $del = $app->bbs->deleteAuthorThumbnail($id); $resp = $app->response(); if ($del) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Edit the notes about the author -> POST /metadata/authors/:id/notes/ JSON */ function edit_author_notes($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('edit_author_notes: invalid author id ' . $id); $app->halt(400, "Bad parameter"); } $app->getLog()->debug('edit_author_notes: ' . $id); $note_data = $app->request()->post(); $app->getLog()->debug('edit_author_notes: note ' . var_export($note_data, true)); try { $markdownParser = new MarkdownExtraParser(); $html = $markdownParser->transformMarkdown($note_data['ntext']); $author = $app->calibre->author($id); $note = $app->bbs->editAuthorNote($id, $author->name, $note_data['mime'], $note_data['ntext']); } catch (Exception $e) { $note = null; } $resp = $app->response(); if (!is_null($note)) { $resp->status(200); $msg = getMessageString('admin_modified'); $note2 = $note->getProperties(); $note2['html'] = $html; $answer = json_encode(array('note' => $note2, 'msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Delete notes about the author -> DELETE /metadata/authors/:id/notes/ JSON */ function del_author_notes($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('del_author_notes: invalid author id ' . $id); $app->halt(400, "Bad parameter"); } $app->getLog()->debug('del_author_notes: ' . $id); $del = $app->bbs->deleteAuthorNote($id); $resp = $app->response(); if ($del) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Add a new author link -> POST /metadata/authors/:id/links JSON */ function new_author_link($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('new_author_link: invalid author id ' . $id); $app->halt(400, "Bad parameter"); } $link_data = $app->request()->post(); $app->getLog()->debug('new_author_link: ' . var_export($link_data, true)); $author = $app->calibre->author($id); $link = null; if (!is_null($author)) { $link = $app->bbs->addAuthorLink($id, $author->name, $link_data['label'], $link_data['url']); } $resp = $app->response(); if (!is_null($link)) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('link' => $link->getProperties(), 'msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /** * Delete an author link -> DELETE /metadata/authors/:id/links/:link/ JSON */ function del_author_link($id, $link) { global $app; // parameter checking if (!is_numeric($id) || !is_numeric($link)) { $app->getLog()->warn('del_author_link: invalid author id ' . $id . ' or link id ' . $link); $app->halt(400, "Bad parameter"); } $app->getLog()->debug('del_author_link: author ' . $id . ', link ' . $link); $ret = $app->bbs->deleteAuthorLink($id, $link); $resp = $app->response(); if ($ret) { $resp->status(200); $msg = getMessageString('admin_modified'); $answer = json_encode(array('msg' => $msg)); $resp->header('Content-type', 'application/json'); } else { $resp->status(500); $resp->header('Content-type', 'text/plain'); $answer = getMessageString('admin_modify_error'); } $resp->header('Content-Length', strlen($answer)); $resp->body($answer); } /********************************************************************* * HTML presentation functions ********************************************************************/ /** * Generate the main page with the 30 most recent titles */ function main() { global $app, $globalSettings; $filter = getFilter(); $books1 = $app->calibre->last30Books($globalSettings['lang'], $globalSettings[PAGE_SIZE], $filter); $books = array_map('checkThumbnail', $books1); $stats = $app->calibre->libraryStats($filter); $app->render('index_last30.html', array( 'page' => mkPage(getMessageString('dl30'), 1, 1), 'books' => $books, 'stats' => $stats)); } # Make a search over all categories. Returns only the first PAGES_SIZE items per category. # If there are more entries per category, there will be a link to the full results. function globalSearch() { global $app, $globalSettings; // TODO check search paramater? $filter = getFilter(); $search = $app->request()->get('search'); $tlb = $app->calibre->titlesSlice($globalSettings['lang'], 0, $globalSettings[PAGE_SIZE], $filter, trim($search)); $tlb_books = array_map('checkThumbnail', $tlb['entries']); $tla = $app->calibre->authorsSlice(0, $globalSettings[PAGE_SIZE], trim($search)); $tla_books = array_map('checkThumbnail', $tla['entries']); $tlt = $app->calibre->tagsSlice(0, $globalSettings[PAGE_SIZE], trim($search)); $tlt_books = array_map('checkThumbnail', $tlt['entries']); $tls = $app->calibre->seriesSlice(0, $globalSettings[PAGE_SIZE], trim($search)); $tls_books = array_map('checkThumbnail', $tls['entries']); $app->render('global_search.html', array( 'page' => mkPage(getMessageString('pagination_search'), 0), 'books' => $tlb_books, 'books_total' => $tlb['total'] == -1 ? 0 : $tlb['total'], 'more_books' => ($tlb['total'] > $globalSettings[PAGE_SIZE]), 'authors' => $tla_books, 'authors_total' => $tla['total'] == -1 ? 0 : $tla['total'], 'more_authors' => ($tla['total'] > $globalSettings[PAGE_SIZE]), 'tags' => $tlt_books, 'tags_total' => $tlt['total'] == -1 ? 0 : $tlt['total'], 'more_tags' => ($tlt['total'] > $globalSettings[PAGE_SIZE]), 'series' => $tls_books, 'series_total' => $tls['total'] == -1 ? 0 : $tls['total'], 'more_series' => ($tls['total'] > $globalSettings[PAGE_SIZE]), 'search' => $search)); } # A list of titles at $index -> /titlesList/:index function titlesSlice($index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($index)) { $app->getLog()->warn('titlesSlice: invalid page id ' . $index); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $search = $app->request()->get('search'); if (isset($search)) { $search = trim($search); } $sort = $app->request()->get('sort'); if (isset($sort) && $sort == 'byReverseDate') { switch ($globalSettings[TITLE_TIME_SORT]) { case TITLE_TIME_SORT_TIMESTAMP: $tl = $app->calibre->timestampOrderedTitlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter, $search); break; case TITLE_TIME_SORT_PUBDATE: $tl = $app->calibre->pubdateOrderedTitlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter, $search); break; case TITLE_TIME_SORT_LASTMODIFIED: $tl = $app->calibre->lastmodifiedOrderedTitlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter, $search); break; default: $app->getLog()->error('titlesSlice: invalid sort order ' . $globalSettings[TITLE_TIME_SORT]); $tl = $app->calibre->timestampOrderedTitlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter, $search); break; } } else { $tl = $app->calibre->titlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter, $search); } $books = array_map('checkThumbnail', $tl['entries']); $app->render('titles.html', array( 'page' => mkPage(getMessageString('titles'), 2, 1), 'url' => 'titleslist', 'books' => $books, 'curpage' => $tl['page'], 'pages' => $tl['pages'], 'search' => $search, 'sort' => $sort)); } # Show a single title > /titles/:id. The ID ist the Calibre ID function title($id) { global $app, $globalSettings; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('title: invalid title id ' . $id); $app->halt(400, "Bad parameter"); } $details = $app->calibre->titleDetails($globalSettings['lang'], $id); if (is_null($details)) { $app->getLog()->warn("title: book not found: " . $id); $app->notFound(); return; } // for people trying to circumvent filtering by direct access if (title_forbidden($details)) { $app->getLog()->warn("title: requested book not allowed for user: " . $id); $app->notFound(); return; } // Show ID links only if there are templates and ID data $idtemplates = $app->bbs->idTemplates(); $id_tmpls = array(); if (count($idtemplates) > 0 && count($details['ids']) > 0) { $show_idlinks = true; foreach ($idtemplates as $idtemplate) { $id_tmpls[$idtemplate->name] = array($idtemplate->val, $idtemplate->label); } } else $show_idlinks = false; $kindle_format = ($globalSettings[KINDLE] == 1) ? $app->calibre->titleGetKindleFormat($id) : NULL; $app->getLog()->debug('titleDetails custom columns: ' . count($details['custom'])); $app->render('title_detail.html', array('page' => mkPage(getMessageString('book_details'), 2, 2), 'book' => $details['book'], 'authors' => $details['authors'], 'series' => $details['series'], 'tags' => $details['tags'], 'formats' => $details['formats'], 'comment' => $details['comment'], 'language' => $details['language'], 'ccs' => (count($details['custom']) > 0 ? $details['custom'] : null), 'show_idlinks' => $show_idlinks, 'ids' => $details['ids'], 'id_templates' => $id_tmpls, 'kindle_format' => $kindle_format, 'kindle_from_email' => $globalSettings[KINDLE_FROM_EMAIL], 'protect_dl' => false) ); } # Return the cover for the book with ID. Calibre generates only JPEGs, so we always return a JPEG. # If there is no cover, return 404. # Route: /titles/:id/cover function cover($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('cover: invalid title id ' . $id); $app->halt(400, "Bad parameter"); } $has_cover = false; $rot = $app->request()->getRootUri(); $book = $app->calibre->title($id); if (is_null($book)) { $app->getLog()->debug("cover: book not found: " + $id); $app->response()->status(404); return; } if ($book->has_cover) { $cover = $app->calibre->titleCover($id); $has_cover = true; } if ($has_cover) { $app->response()->status(200); $app->response()->header('Content-type', 'image/jpeg;base64'); $app->response()->header('Content-Length', filesize($cover)); readfile($cover); } else { $app->response()->status(404); } } # Return the cover for the book with ID. Calibre generates only JPEGs, so we always return a JPEG. # If there is no cover, return 404. # Route: /titles/:id/thumbnail function thumbnail($id) { global $app, $globalSettings; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('thumbnail: invalid title id ' . $id); $app->halt(400, "Bad parameter"); } $app->getLog()->debug('thumbnail: ' . $id); $has_cover = false; $rot = $app->request()->getRootUri(); $book = $app->calibre->title($id); if (is_null($book)) { $app->getLog()->error("thumbnail: book not found: " + $id); $app->response()->status(404); return; } if ($book->has_cover) { $cover = $app->calibre->titleCover($id); $thumb = $app->bbs->titleThumbnail($id, $cover, $globalSettings[THUMB_GEN_CLIPPED]); $app->getLog()->debug('thumbnail: thumb found ' . $thumb); $has_cover = true; } if ($has_cover) { $app->response()->status(200); $app->response()->header('Content-type', 'image/png;base64'); $app->response()->header('Content-Length', filesize($thumb)); readfile($thumb); } else { $app->response()->status(404); } } # Return the selected file for the book with ID. # Route: /titles/:id/file/:file function book($id, $file) { global $app, $globalSettings; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('book: invalid title id ' . $id); $app->halt(400, "Bad parameter"); } // TODO check file parameter? $details = $app->calibre->titleDetails($globalSettings['lang'], $id); if (is_null($details)) { $app->getLog()->warn("book: no book found for " . $id); $app->notFound(); } // for people trying to circumvent filtering by direct access if (title_forbidden($details)) { $app->getLog()->warn("book: requested book not allowed for user: " . $id); $app->notFound(); return; } $real_bookpath = $app->calibre->titleFile($id, $file); $contentType = Utilities::titleMimeType($real_bookpath); if (is_authenticated()) { $app->getLog()->info("book download by " . $app->auth->getUserName() . " for " . $real_bookpath . " with metadata update = " . $globalSettings[METADATA_UPDATE]); } else { $app->getLog()->info("book download for " . $real_bookpath . " with metadata update = " . $globalSettings[METADATA_UPDATE]); } if ($contentType == Utilities::MIME_EPUB && $globalSettings[METADATA_UPDATE]) { if ($details['book']->has_cover == 1) $cover = $app->calibre->titleCover($id); else $cover = null; // If an EPUB update the metadata $mdep = new MetadataEpub($real_bookpath); $mdep->updateMetadata($details, $cover); $bookpath = $mdep->getUpdatedFile(); $app->getLog()->debug("book(e): file " . $bookpath); $app->getLog()->debug("book(e): type " . $contentType); $booksize = filesize($bookpath); $app->getLog()->debug("book(e): size " . $booksize); if ($booksize > 0) header("Content-Length: " . $booksize); header("Content-Type: " . $contentType); header("Content-Disposition: attachment; filename=\"" . $file . "\""); header("Content-Description: File Transfer"); header("Content-Transfer-Encoding: binary"); readfile_chunked($bookpath); } else { // Else send the file as is $bookpath = $real_bookpath; $app->getLog()->debug("book: file " . $bookpath); $app->getLog()->debug("book: type " . $contentType); $booksize = filesize($bookpath); $app->getLog()->debug("book: size " . $booksize); header("Content-Length: " . $booksize); header("Content-Type: " . $contentType); header("Content-Disposition: attachment; filename=\"" . $file . "\""); header("Content-Description: File Transfer"); header("Content-Transfer-Encoding: binary"); readfile_chunked($bookpath); } } # Send the selected file to a Kindle e-mail address # Route: /titles/:id/kindle/:file function kindle($id, $file) { global $app, $globalSettings; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('kindle: invalid title id ' . $id); $app->halt(400, "Bad parameter"); } // TODO check file parameter? $book = $app->calibre->title($id); if (is_null($book)) { $app->getLog()->debug("kindle: book not found: " . $id); $app->notFound(); } $details = $app->calibre->titleDetails($globalSettings['lang'], $id); $filename = ""; if ($details['series'] != null) { $filename .= $details['series'][0]->name; $filename .= "[" . $details['book']->series_index . "] "; } $filename .= $details['book']->title; $filename .= " - "; foreach ($details['authors'] as $author) { $filename .= $author->name; } $filename .= ".mobi"; # Validate request e-mail format $to_email = $app->request()->post('email'); if (!isEMailValid($to_email)) { $app->getLog()->debug("kindle: invalid email, " . $to_email); $app->response()->status(400); return; } else { $app->deleteCookie(KINDLE_COOKIE); $bookpath = $app->calibre->titleFile($id, $file); $app->getLog()->debug("kindle: requested file " . $bookpath); if ($globalSettings[MAILER] == Mailer::SMTP) { $mail = array('username' => $globalSettings[SMTP_USER], 'password' => $globalSettings[SMTP_PASSWORD], 'smtp-server' => $globalSettings[SMTP_SERVER], 'smtp-port' => $globalSettings[SMTP_PORT]); if ($globalSettings[SMTP_ENCRYPTION] == 1) $mail['smtp-encryption'] = Mailer::SSL; elseif ($globalSettings[SMTP_ENCRYPTION] == 2) { $mail['smtp-encryption'] = Mailer::TLS; } $app->getLog()->debug('kindle mail config: ' . var_export($mail, true)); $mailer = new Mailer(Mailer::SMTP, $mail); } elseif ($globalSettings[MAILER] == Mailer::SENDMAIL) { $mailer = new Mailer(Mailer::SENDMAIL); } else { $mailer = new Mailer(Mailer::MAIL); } $send_success = 0; try { $message = $mailer->createBookMessage($bookpath, $globalSettings[DISPLAY_APP_NAME], $to_email, $globalSettings[KINDLE_FROM_EMAIL], $filename); $send_success = $mailer->sendMessage($message); if ($send_success == 0) $app->getLog()->warn('kindle: book delivery to ' . $to_email . ' failed, dump: ' . $mailer->getDump()); else $app->getLog()->debug('kindle: book delivered to ' . $to_email . ', result ' . $send_success); # if there was an exception, log it and return gracefully } catch (Exception $e) { $app->getLog()->warn('kindle: Email exception ' . $e->getMessage()); $app->getLog()->warn('kindle: Mail dump ' . $mailer->getDump()); } # Store e-mail address in cookie so user needs to enter it only once $app->setCookie(KINDLE_COOKIE, $to_email); if ($send_success > 0) echo getMessageString('send_success'); else $app->response()->status(503); } } /** * A list of authors at $index -> /authorslist/:index * @param int $index author list page index */ function authorsSlice($index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($index)) { $app->getLog()->warn('authorsSlice: invalid page id ' . $index); $app->halt(400, "Bad parameter"); } $search = $app->request()->get('search'); if (isset($search)) $tl = $app->calibre->authorsSlice($index, $globalSettings[PAGE_SIZE], trim($search)); else $tl = $app->calibre->authorsSlice($index, $globalSettings[PAGE_SIZE]); foreach ($tl['entries'] as $author) { $author->thumbnail = $app->bbs->getAuthorThumbnail($author->id); if ($author->thumbnail) $app->getLog()->debug('authorsSlice thumbnail ' . var_export($author->thumbnail->url, true)); } $app->render('authors.html', array( 'page' => mkPage(getMessageString('authors'), 3, 1), 'url' => 'authorslist', 'authors' => $tl['entries'], 'curpage' => $tl['page'], 'pages' => $tl['pages'], 'search' => $search)); } /** * Details for a single author -> /authors/:id * @param int $id author id * @deprecated since 0.9.3 */ function author($id) { global $app; $details = $app->calibre->authorDetails($id); if (is_null($details)) { $app->getLog()->debug("no author"); $app->notFound(); } $app->render('author_detail.html', array( 'page' => mkPage(getMessageString('author_details'), 3, 2), 'author' => $details['author'], 'books' => $details['books'])); } /** * Details for a single author -> /authors/:id/:page/ * Shows the detail data for the author plus a paginated list of books * * @param integer $id author id * @param integer $index page index for book list * @return HTML page */ function authorDetailsSlice($id, $index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($id) || !is_numeric($index)) { $app->getLog()->warn('authorDetailsSlice: invalid author id ' . $id . ' or page id ' . $index); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $tl = $app->calibre->authorDetailsSlice($globalSettings['lang'], $id, $index, $globalSettings[PAGE_SIZE], $filter); if (is_null($tl)) { $app->getLog()->debug('no author ' . $id); $app->notFound(); } $books = array_map('checkThumbnail', $tl['entries']); $author = $tl['author']; $author->thumbnail = $app->bbs->getAuthorThumbnail($id); $note = $app->bbs->authorNote($id); if (!is_null($note)) $author->notes_source = $note->ntext; else $author->notes_source = null; if (!empty($author->notes_source)) { $markdownParser = new MarkdownExtraParser(); $author->notes = $markdownParser->transformMarkdown($author->notes_source); } else { $author->notes = null; } $author->links = $app->bbs->authorLinks($id); $app->render('author_detail.html', array( 'page' => mkPage(getMessageString('author_details'), 3, 2), 'url' => 'authors/' . $id, 'author' => $tl['author'], 'books' => $books, 'curpage' => $tl['page'], 'pages' => $tl['pages'], 'isadmin' => is_admin())); } /** * Notes for a single author -> /authors/:id/notes/ * * @param int $id author id */ function authorNotes($id) { global $app; // parameter checking if (!is_numeric($id)) { $app->getLog()->warn('authorNotes: invalid author id ' . $id); $app->halt(400, "Bad parameter"); } $author = $app->calibre->author($id); if (is_null($author)) { $app->getLog()->debug('authorNotes: author id not found ' . $id); $app->notFound(); } $note = $app->bbs->authorNote($id); if (!is_null($note)) $author->notes_source = $note->ntext; else $author->notes_source = null; if (!empty($author->notes_source)) { $markdownParser = new MarkdownExtraParser(); $author->notes = $markdownParser->transformMarkdown($author->notes_source); } else { $author->notes = null; } $app->render('author_notes.html', array( 'page' => mkPage(getMessageString('author_notes'), 3, 2), 'url' => 'authors/' . $id, 'author' => $author, 'isadmin' => is_admin())); } /** * Return a HTML page of series at page $index. * @param int $index =0 page index into series list */ function seriesSlice($index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($index)) { $app->getLog()->warn('seriesSlice: invalid series index ' . $index); $app->halt(400, "Bad parameter"); } $search = $app->request()->get('search'); if (isset($search)) { $app->getLog()->debug('seriesSlice: search ' . $search); $tl = $app->calibre->seriesSlice($index, $globalSettings[PAGE_SIZE], trim($search)); } else { $tl = $app->calibre->seriesSlice($index, $globalSettings[PAGE_SIZE]); } $app->render('series.html', array( 'page' => mkPage(getMessageString('series'), 5, 1), 'url' => 'serieslist', 'series' => $tl['entries'], 'curpage' => $tl['page'], 'pages' => $tl['pages'], 'search' => $search)); $app->getLog()->debug('seriesSlice ended'); } /** * Return a HTML page with details of series $id, /series/:id * @param int $id series id * @deprecated since 0.9.3 */ function series($id) { global $app; $details = $app->calibre->seriesDetails($id); if (is_null($details)) { $app->getLog()->debug('no series ' . $id); $app->notFound(); } $app->render('series_detail.html', array( 'page' => mkPage(getMessageString('series_details'), 5, 3), 'series' => $details['series'], 'books' => $details['books'])); } /** * Details for a single series -> /series/:id/:page/ * Shows the detail data for the series plus a paginated list of books * * @param int $id series id * @param int $index page index for books */ function seriesDetailsSlice($id, $index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($id) || !is_numeric($index)) { $app->getLog()->warn('seriesDetailsSlice: invalid series id ' . $id . ' or page id ' . $index); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $tl = $app->calibre->seriesDetailsSlice($globalSettings['lang'], $id, $index, $globalSettings[PAGE_SIZE], $filter); if (is_null($tl)) { $app->getLog()->debug('seriesDetailsSlice: no series ' . $id); $app->notFound(); } $books = array_map('checkThumbnail', $tl['entries']); $app->render('series_detail.html', array( 'page' => mkPage(getMessageString('series_details'), 5, 2), 'url' => 'series/' . $id, 'series' => $tl['series'], 'books' => $books, 'curpage' => $tl['page'], 'pages' => $tl['pages'])); } /** * A list of tags at $index -> /tagslist/:index * @param int $index */ function tagsSlice($index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($index)) { $app->getLog()->warn('tagsSlice: invalid page id ' . $index); $app->halt(400, "Bad parameter"); } $search = $app->request()->get('search'); if (isset($search)) $tl = $app->calibre->tagsSlice($index, $globalSettings[PAGE_SIZE], trim($search)); else $tl = $app->calibre->tagsSlice($index, $globalSettings[PAGE_SIZE]); $app->render('tags.html', array( 'page' => mkPage(getMessageString('tags'), 4, 1), 'url' => 'tagslist', 'tags' => $tl['entries'], 'curpage' => $tl['page'], 'pages' => $tl['pages'], 'search' => $search)); } # Details for a single tag -> /tags/:id/:page # @deprecated since 0.9.3 function tag($id) { global $app; $details = $app->calibre->tagDetails($id); if (is_null($details)) { $app->getLog()->debug("no tag"); $app->notFound(); } $app->render('tag_detail.html', array( 'page' => mkPage(getMessageString('tag_details'), 4, 3), 'tag' => $details['tag'], 'books' => $details['books'])); } /** * Details for a single tag -> /tags/:id/:page/ * Shows the detail data for the tag plus a paginated list of books * * @param integer $id series id * @param integer $index page index for books * @return HTML page */ function tagDetailsSlice($id, $index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($id) || !is_numeric($index)) { $app->getLog()->warn('tagsDetailsSlice: invalid tag id ' . $id . ' or page id ' . $index); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $tl = $app->calibre->tagDetailsSlice($globalSettings['lang'], $id, $index, $globalSettings[PAGE_SIZE], $filter); if (is_null($tl)) { $app->getLog()->debug('no tag ' . $id); $app->notFound(); } $books = array_map('checkThumbnail', $tl['entries']); $app->render('tag_detail.html', array( 'page' => mkPage(getMessageString('tag_details'), 4, 2), 'url' => 'tags/' . $id, 'tag' => $tl['tag'], 'books' => $books, 'curpage' => $tl['page'], 'pages' => $tl['pages'])); } /********************************************************************* * OPDS Catalog functions ********************************************************************/ /** * Generate and send the OPDS root navigation catalog */ function opdsRoot() { global $app; $gen = mkOpdsGenerator($app); $cat = $gen->rootCatalog(NULL); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_NAV); } /** * Generate and send the OPDS 'newest' catalog. This catalog is an * acquisition catalog with a subset of the title details. * * Note: OPDS acquisition feeds need an acquisition link for every item, * so books without formats are removed from the output. */ function opdsNewest() { global $app, $globalSettings; $filter = getFilter(); $just_books = $app->calibre->last30Books($globalSettings['lang'], $globalSettings[PAGE_SIZE], $filter); $books1 = array(); foreach ($just_books as $book) { $record = $app->calibre->titleDetailsOpds($book); if (!empty($record['formats'])) array_push($books1, $record); } $books = array_map('checkThumbnailOpds', $books1); $gen = mkOpdsGenerator($app); $cat = $gen->newestCatalog(NULL, $books, false); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_ACQ); } /** * Return a page of the titles. * * Note: OPDS acquisition feeds need an acquisition link for every item, * so books without formats are removed from the output. * * @param integer $index =0 page index */ function opdsByTitle($index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($index)) { $app->getLog()->warn('opdsByTitle: invalid page id ' . $index); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $search = $app->request()->get('search'); if (isset($search)) $tl = $app->calibre->titlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter, $search); else $tl = $app->calibre->titlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter); $books1 = $app->calibre->titleDetailsFilteredOpds($tl['entries']); $books = array_map('checkThumbnailOpds', $books1); $gen = mkOpdsGenerator($app); $cat = $gen->titlesCatalog(NULL, $books, false, $tl['page'], getNextSearchPage($tl), getLastSearchPage($tl)); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_ACQ); } /** * Return a page with author names initials */ function opdsByAuthorInitial() { global $app; $initials = $app->calibre->authorsInitials(); $gen = mkOpdsGenerator($app); $cat = $gen->authorsRootCatalog(NULL, $initials); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_NAV); } /** * Return a page with author names for a initial * @param string $initial single uppercase character */ function opdsByAuthorNamesForInitial($initial) { global $app; // parameter checking if (!(ctype_upper($initial))) { $app->getLog()->warn('opdsByAuthorNamesForInitial: invalid initial ' . $initial); $app->halt(400, "Bad parameter"); } $authors = $app->calibre->authorsNamesForInitial($initial); $gen = mkOpdsGenerator($app); $cat = $gen->authorsNamesForInitialCatalog(NULL, $authors, $initial); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_NAV); } /** * Return a feed with partial acquisition entries for the author's books * @param string initial initial character * @param int id author id * @param int page page number */ function opdsByAuthor($initial, $id, $page) { global $app, $globalSettings; // parameter checking if (!is_numeric($id) || !is_numeric($page)) { $app->getLog()->warn('opdsByAuthor: invalid author id ' . $id . ' or page id ' . $page); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $tl = $app->calibre->authorDetailsSlice($globalSettings['lang'], $id, $page, $globalSettings[PAGE_SIZE], $filter); $app->getLog()->debug('opdsByAuthor 1 ' . var_export($tl, true)); $books1 = $app->calibre->titleDetailsFilteredOpds($tl['entries']); $books = array_map('checkThumbnailOpds', $books1); $app->getLog()->debug('opdsByAuthor 2 ' . var_export($books, true)); $gen = mkOpdsGenerator($app); $cat = $gen->booksForAuthorCatalog(NULL, $books, $initial, $tl['author'], false, $tl['page'], getNextSearchPage($tl), getLastSearchPage($tl)); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_ACQ); } /** * Return a page with tag initials */ function opdsByTagInitial() { global $app; $initials = $app->calibre->tagsInitials(); $gen = mkOpdsGenerator($app); $cat = $gen->tagsRootCatalog(NULL, $initials); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_NAV); } /** * Return a page with author names for a initial * @param string $initial single uppercase character */ function opdsByTagNamesForInitial($initial) { global $app; // parameter checking if (!(ctype_upper($initial))) { $app->getLog()->warn('opdsByTagNamesForInitial: invalid initial ' . $initial); $app->halt(400, "Bad parameter"); } $tags = $app->calibre->tagsNamesForInitial($initial); $gen = mkOpdsGenerator($app); $cat = $gen->tagsNamesForInitialCatalog(NULL, $tags, $initial); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_NAV); } /** * Return a feed with partial acquisition entries for the tags's books * @param string $initial initial character * @param int $id tag id * @param int $page page index */ function opdsByTag($initial, $id, $page) { global $app, $globalSettings; // parameter checking if (!is_numeric($id) || !is_numeric($page)) { $app->getLog()->warn('opdsByTag: invalid tag id ' . $id . ' or page id ' . $page); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $tl = $app->calibre->tagDetailsSlice($globalSettings['lang'], $id, $page, $globalSettings[PAGE_SIZE], $filter); $books1 = $app->calibre->titleDetailsFilteredOpds($tl['entries']); $books = array_map('checkThumbnailOpds', $books1); $gen = mkOpdsGenerator($app); $cat = $gen->booksForTagCatalog(NULL, $books, $initial, $tl['tag'], false, $tl['page'], getNextSearchPage($tl), getLastSearchPage($tl)); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_ACQ); } /** * Return a page with series initials */ function opdsBySeriesInitial() { global $app; $initials = $app->calibre->seriesInitials(); $gen = mkOpdsGenerator($app); $cat = $gen->seriesRootCatalog(NULL, $initials); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_NAV); } /** * Return a page with author names for a initial * @param string $initial "all" or single uppercase character */ function opdsBySeriesNamesForInitial($initial) { global $app; // parameter checking if (!($initial == 'all' || ctype_upper($initial))) { $app->getLog()->warn('opdsBySeriesNamesForInitial: invalid initial ' . $initial); $app->halt(400, "Bad parameter"); } $tags = $app->calibre->seriesNamesForInitial($initial); $gen = mkOpdsGenerator($app); $cat = $gen->seriesNamesForInitialCatalog(NULL, $tags, $initial); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_NAV); } /** * Return a feed with partial acquisition entries for the series' books * @param string initial initial character * @param int id tag id * @param int page page index */ function opdsBySeries($initial, $id, $page) { global $app, $globalSettings; // parameter checking if (!is_numeric($id) || !is_numeric($page)) { $app->getLog()->warn('opdsBySeries: invalid series id ' . $id . ' or page id ' . $page); $app->halt(400, "Bad parameter"); } $filter = getFilter(); $tl = $app->calibre->seriesDetailsSlice($globalSettings['lang'], $id, $page, $globalSettings[PAGE_SIZE], $filter); $books1 = $app->calibre->titleDetailsFilteredOpds($tl['entries']); $books = array_map('checkThumbnailOpds', $books1); $gen = mkOpdsGenerator($app); $cat = $gen->booksForSeriesCatalog(NULL, $books, $initial, $tl['series'], false, $tl['page'], getNextSearchPage($tl), getLastSearchPage($tl)); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_ACQ); } /** * Format and send the OpenSearch descriptor document */ function opdsSearchDescriptor() { global $app; $gen = mkOpdsGenerator($app); $cat = $gen->searchDescriptor(NULL, '/opds/searchlist/0/'); mkOpdsResponse($app, $cat, OpdsGenerator::OPENSEARCH_MIME); } /** * Create and send the catalog page for the current search criteria. * The search criteria is a GET paramter string. * * @param integer $index index of page in search */ function opdsBySearch($index = 0) { global $app, $globalSettings; // parameter checking if (!is_numeric($index)) { $app->getLog()->warn('opdsBySearch: invalid page id ' . $index); $app->halt(400, "Bad parameter"); } $search = $app->request()->get('search'); if (!isset($search)) { $app->getLog()->error('opdsBySearch called without search criteria, page ' . $index); // 400 Bad request $app->response()->status(400); return; } $filter = getFilter(); $tl = $app->calibre->titlesSlice($globalSettings['lang'], $index, $globalSettings[PAGE_SIZE], $filter, $search); $books1 = $app->calibre->titleDetailsFilteredOpds($tl['entries']); $books = array_map('checkThumbnailOpds', $books1); $gen = mkOpdsGenerator($app); $cat = $gen->searchCatalog(NULL, $books, false, $tl['page'], getNextSearchPage($tl), getLastSearchPage($tl), $search, $tl['total'], $globalSettings[PAGE_SIZE]); mkOpdsResponse($app, $cat, OpdsGenerator::OPDS_MIME_ACQ); } /********************************************************************* * Utility and helper functions, private ********************************************************************/ function checkThumbnail($book) { global $app; $book->thumbnail = $app->bbs->isTitleThumbnailAvailable($book->id); return $book; } function checkThumbnailOpds($record) { global $app; $record['book']->thumbnail = $app->bbs->isTitleThumbnailAvailable($record['book']->id); return $record; } function getFilter() { global $app; $lang = null; $tag = null; if (is_authenticated()) { $user = $app->auth->getUserData(); $app->getLog()->debug('getFilter: ' . var_export($user, true)); if (!empty($user['languages'])) $lang = $app->calibre->getLanguageId($user['languages']); if (!empty($user['tags'])) $tag = $app->calibre->getTagId($user['tags']); $app->getLog()->debug('getFilter: Using language ' . $lang . ', tag ' . $tag); } return new CalibreFilter($lang, $tag); } # Initialize the OPDS generator function mkOpdsGenerator($app) { global $appversion, $globalSettings; $root = rtrim($app->request()->getUrl() . $app->request()->getRootUri(), "/"); $gen = new OpdsGenerator($root, $appversion, $app->calibre->calibre_dir, date(DATE_ATOM, $app->calibre->calibre_last_modified), $globalSettings['l10n']); return $gen; } # Create and send the typical OPDS response function mkOpdsResponse($app, $content, $type) { $resp = $app->response(); $resp->status(200); $resp->header('Content-type', $type); $resp->header('Content-Length', strlen($content)); $resp->body($content); } # Utility function to fill the page array function mkPage($subtitle = '', $menu = 0, $level = 0) { global $app, $globalSettings; if ($subtitle == '') $title = $globalSettings[DISPLAY_APP_NAME]; else $title = $globalSettings[DISPLAY_APP_NAME] . $globalSettings['sep'] . $subtitle; $rot = $app->request()->getRootUri(); $auth = is_authenticated(); if ($globalSettings[LOGIN_REQUIRED]) $adm = is_admin(); else $adm = true; # the admin button should be always visible if no login is required $page = array('title' => $title, 'rot' => $rot, 'h1' => $subtitle, 'version' => $globalSettings['version'], 'glob' => $globalSettings, 'menu' => $menu, 'level' => $level, 'auth' => $auth, 'admin' => $adm); return $page; } /** * Checks if a title is available to the current users * @param details output of BicBucStriim::title_details() * @return true if the title is not availble for the user, else false */ function title_forbidden($book_details) { global $app; if (!is_authenticated()) return false; $user = $app->auth->getUserData(); if (empty($user['languages']) && empty($user['tags'])) { return false; } else { if (!empty($user['languages'])) { $lang_found = false; foreach ($book_details['langcodes'] as $langcode) { if ($langcode === $user['languages']) { $lang_found = true; break; } } if (!$lang_found) { return true; } } if (!empty($user['tags'])) { $tag_found = false; foreach ($book_details['tags'] as $tag) { if ($tag->name === $user['tags']) { $tag_found = true; break; } } if ($tag_found) { return true; } } return false; } } /** * Return a localized message string for $id. * * If there is no defined message for $id in the current language the function * looks for an alterantive in English. If that also fails an error message * is returned. * * @param string $id message id * @return string localized message string */ function getMessageString($id) { global $globalSettings; $msg = $globalSettings['l10n']->message($id); return $msg; } /** * Calcluate the next page number for search results * @param array $tl search result * @return int page index or NULL */ function getNextSearchPage($tl) { if ($tl['page'] < $tl['pages'] - 1) $nextPage = $tl['page'] + 1; else $nextPage = NULL; return $nextPage; } /** * Caluclate the last page numberfor search results * @param array $tl search result * @return int page index */ function getLastSearchPage($tl) { if ($tl['pages'] == 0) $lastPage = 0; else $lastPage = $tl['pages'] - 1; return $lastPage; } /** * Returns the user language, priority: * 1. Language in $_GET['lang'] * 2. Language in $_SESSION['lang'] * 3. HTTP_ACCEPT_LANGUAGE * 4. Fallback language * * @param array $allowedLangs list of existing languages * @param string $fallbackLang id of the fallback language if nothing helps * @return string the user language, like 'de' or 'en' */ function getUserLang($allowedLangs, $fallbackLang) { // reset user_lang array $userLangs = array(); // 2nd highest priority: GET parameter 'lang' if (isset($_GET['lang']) && is_string($_GET['lang'])) { $userLangs[] = $_GET['lang']; } // 3rd highest priority: SESSION parameter 'lang' if (isset($_SESSION['lang']) && is_string($_SESSION['lang'])) { $userLangs[] = $_SESSION['lang']; } // 4th highest priority: HTTP_ACCEPT_LANGUAGE if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $part) { $userLangs[] = strtolower(substr($part, 0, 2)); } } // Lowest priority: fallback $userLangs[] = $fallbackLang; foreach ($allowedLangs as $al) { if ($userLangs[0] == $al) return $al; } return $fallbackLang; } /** * Is the key in globalSettings? * @param string $key key for config value * @return boolean true = key available */ function has_global_setting($key) { global $globalSettings; return (isset($globalSettings[$key]) && !empty($globalSettings[$key])); } /** * Is there a valid - existing - Calibre directory? * @return boolean true if available */ function has_valid_calibre_dir() { global $globalSettings; return (has_global_setting(CALIBRE_DIR) && Calibre::checkForCalibre($globalSettings[CALIBRE_DIR])); } /** * Check if the current user was authenticated * @return boolean true if authenticated, else false */ function is_authenticated() { global $app; return (is_object($app->auth) && $app->auth->isValid()); } /** * Check for admin permissions. Currently this is only the user * admin, ID 1. * @return boolean true if admin user, else false */ function is_admin() { global $app; if (is_authenticated()) { $user = $app->auth->getUserData(); return ($user['role'] === '1'); } else { return false; } } # Utility function to serve files function readfile_chunked($filename) { global $app; $app->getLog()->debug('readfile_chunked ' . $filename); $handle = fopen($filename, 'rb'); if ($handle === false) { return false; } while (!feof($handle)) { $buffer = fread($handle, 1024 * 1024); echo $buffer; ob_flush(); flush(); } $status = fclose($handle); return $status; } # Check for valid email address format function isEMailValid($mail) { return (filter_var($mail, FILTER_VALIDATE_EMAIL) !== FALSE); } ?>