/** * @route file_manager/{string}/move * @param string $cabin */ public function moveFile(string $cabin = '') { $dir = $this->determinePath($cabin); if (!\in_array($cabin, $this->getCabinNamespaces())) { \Airship\redirect($this->airship_cabin_prefix); } $this->setTemplateExtraData($cabin); if (empty($_GET['file'])) { $this->commonMoveDir($dir, $cabin); return; } $this->commonMoveFile($_GET['file'], $dir, $cabin); }
/** * @route gadgets/universal */ public function manageUniversal() { $cabins = $this->getCabinNamespaces(); $gadgets = \Airship\loadJSON(ROOT . '/config/gadgets.json'); if (!$this->can('update')) { \Airship\redirect($this->airship_cabin_prefix . '/gadgets'); } $post = $this->post(GadgetsFilter::fromConfig(\array_keys($gadgets))); if ($post) { if ($this->updateUniversalGadgets($gadgets, $post)) { \Airship\clear_cache(); \Airship\redirect($this->airship_cabin_prefix . '/gadgets/universal'); } } $this->lens('gadget_manage', ['cabins' => $cabins, 'gadgets' => $gadgets, 'title' => \__('Manage Universal Gadgets')]); }
/** * @route motifs/{string} * * @param string $cabinName */ public function manage(string $cabinName = '') { $cabins = $this->getCabinNamespaces(); if (!\in_array($cabinName, $cabins)) { \Airship\redirect($this->airship_cabin_prefix . '/motifs'); } if (!$this->can('update')) { \Airship\redirect($this->airship_cabin_prefix . '/motifs'); } $motifs = \Airship\loadJSON(ROOT . '/Cabin/' . $cabinName . '/config/motifs.json'); $post = $this->post(MotifsFilter::fromConfig(\array_keys($motifs))); if ($post) { if ($this->updateMotifs($motifs, $post, $cabinName)) { \Airship\clear_cache(); \Airship\redirect($this->airship_cabin_prefix . '/motifs/cabin/' . $cabinName); } } $this->lens('motif_manage', ['cabin_name' => $cabinName, 'cabins' => $cabins, 'motifs' => $motifs, 'title' => \__('Motifs for %s', 'default', Util::noHTML($cabinName))]); }
/** * @route author/files/{id}/{string}/move * @param string $authorId * @param string $cabin */ public function moveFile(string $authorId, string $cabin = '') { $this->loadAuthorInfo((int) $authorId); $this->files->ensureDirExists($this->root_dir, $cabin); $this->files->ensureDirExists($this->root_dir . '/photos', $cabin); $dir = $this->determinePath($cabin); if (!\in_array($cabin, $this->getCabinNamespaces())) { \Airship\redirect($this->airship_cabin_prefix); } if (empty($_GET['file'])) { return $this->commonMoveDir($dir, $cabin); } return $this->commonMoveFile($_GET['file'], $dir, $cabin); }
if (@\is_readable(dirname(__DIR__) . '/tmp/installing.json') || !\file_exists(dirname(__DIR__) . '/config/databases.json')) { include dirname(__DIR__) . '/Installer/launch.php'; exit; } /** * Load the bare minimum: */ require_once \dirname(__DIR__) . '/preload.php'; $start = \microtime(true); if (empty($_POST)) { /** * Let's get rid of trailing slashes in URLs without POST data */ $sliceAt = Binary::safeStrlen($_SERVER['REQUEST_URI']) - 1; if ($sliceAt > 0 && $_SERVER['REQUEST_URI'][$sliceAt] === '/') { \Airship\redirect('/' . \trim($_SERVER['REQUEST_URI'], '/')); } /** * Let's handle static content caching */ if (\extension_loaded('apcu')) { $staticCache = (new MemoryCache())->personalize('staticPage:'); $cspCache = (new MemoryCache())->personalize('contentSecurityPolicy:'); } else { if (!\is_dir(ROOT . '/tmp/cache/static')) { require_once ROOT . '/tmp_dirs.php'; } $staticCache = new FileCache(ROOT . '/tmp/cache/static'); $cspCache = new FileCache(ROOT . '/tmp/cache/csp_static'); } $port = $_SERVER['HTTP_PORT'] ?? '';
/** * Do not allow insecure HTTP request to proceed * * @param string $scheme * @return bool */ protected static function forceHTTPS(string $scheme = '') : bool { if (!self::isHTTPSConnection($scheme)) { // Should we redirect to an HTTPS endpoint? \Airship\redirect('https://' . $_SERVER['HTTP_HOST'] . '/' . $_SERVER['REQUEST_URI'], $_GET ?? []); } return true; }
/** * The last phase of the installer */ protected function finalShutdown() { $this->autoSave = false; $this->finalDefaultPages(); \unlink(ROOT . '/tmp/installing.json'); foreach (\glob(ROOT . '/tmp/cache/*.json') as $f) { \unlink($f); } \Airship\redirect('/'); }
/** * Manage the users that have access to this author * * @route author/users/{id} * @param string $authorId */ public function users(string $authorId = '') { $authorId = (int) $authorId; if ($this->isSuperUser()) { $inCharge = true; } else { $authorsForUser = $this->author->getAuthorIdsForUser($this->getActiveUserId()); // Check if (!\in_array($authorId, $authorsForUser)) { \Airship\redirect($this->airship_cabin_prefix . '/author'); } $inCharge = $this->author->userIsOwner($authorId); } // Only someone in charge can add/remove users: if ($inCharge) { $post = $this->post(new UsersFilter()); if ($post) { if ($this->manageAuthorUsers($authorId, $post)) { \Airship\redirect($this->airship_cabin_prefix . '/author/users/' . $authorId); } } } $this->lens('author/users', ['author' => $this->author->getById($authorId), 'inCharge' => $inCharge, 'users' => $this->author->getUsersForAuthor($authorId)]); }
/** * Update Cabin configuration * * @route cabins/manage{_string} * @param string $cabinName */ public function manage(string $cabinName = '') { if (!$this->isSuperUser()) { // Admins only! \Airship\redirect($this->airship_cabin_prefix); } if (!\in_array($cabinName, $this->getCabinNamespaces())) { // Invalid cabin name \Airship\redirect($this->airship_cabin_prefix . '/cabins'); } $this->setTemplateExtraData($cabinName); if (!$this->ensureCabinLinkExists($cabinName)) { \Airship\json_response(['error' => 'Could not create symlink']); } $cabin = \Airship\loadJSON(ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'cabins.json'); // Apply the cabin's input filter: $filterName = '\\Airship\\Cabin\\' . $cabinName . '\\ConfigFilter'; if (\class_exists($filterName)) { $filter = new $filterName(); } else { $filter = (new GeneralFilterContainer())->addFilter('content_security_policy', new ArrayFilter()); } $post = $this->post($filter); if (!empty($post)) { if ($this->saveSettings($cabinName, $cabin, $post)) { \Airship\redirect($this->airship_cabin_prefix . '/cabins/manage/' . $cabinName); } } $settings = []; foreach ($cabin as $path => $data) { if ($data['name'] === $cabinName) { $settings['cabin'] = $data; $settings['cabin']['path'] = $path; break; } } if (empty($settings['cabin'])) { // Cabin not found \Airship\redirect($this->airship_cabin_prefix); } $settings['content_security_policy'] = $this->loadJSONConfigFile($cabinName, 'content_security_policy.json'); $settings['cabin_extra'] = \Airship\loadJSON(ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'Cabin' . DIRECTORY_SEPARATOR . $cabinName . DIRECTORY_SEPARATOR . 'config.json'); $gadgets = ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'Cabin' . DIRECTORY_SEPARATOR . $cabinName . DIRECTORY_SEPARATOR . 'gadgets.json'; if (!\file_exists($gadgets)) { \file_put_contents($gadgets, '[]'); } $settings['gadgets'] = \Airship\loadJSON($gadgets); $settings['motifs'] = $this->getCabinsMotifs($cabinName); $settings['twig_vars'] = \Airship\loadJSON(ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'Cabin' . DIRECTORY_SEPARATOR . $cabinName . DIRECTORY_SEPARATOR . 'twig_vars.json'); $this->lens('cabin_manage', ['name' => $cabinName, 'config' => $settings]); }
/** * Edit a user's information * * @route crew/users/edit/{id} * @param string $userId */ public function editUser(string $userId = '') { $userId = (int) $userId; $user = $this->account->getUserAccount($userId, true); $post = $this->post(new EditUserFilter()); if ($post) { if ($this->account->editUser($userId, $post)) { \Airship\redirect($this->airship_cabin_prefix . '/crew/users'); } } $this->lens('crew/user_edit', ['active_link' => 'bridge-link-admin-crew-users', 'user' => $user, 'groups' => $this->account->getGroupTree()]); }
/** * @route admin/settings */ public function manageSettings() { $state = State::instance(); $settings = ['universal' => $state->universal]; $post = $this->post(new SettingsFilter()); if (!empty($post)) { if ($this->saveSettings($post)) { \Airship\clear_cache(); \Airship\redirect($this->airship_cabin_prefix . '/admin/settings', ['msg' => 'saved']); } else { $this->log('Could not save new settings', LogLevel::ALERT); } } // Load individual files... $settings['cabins'] = $this->loadJSONConfigFile('cabins.json'); $settings['content_security_policy'] = $this->loadJSONConfigFile('content_security_policy.json'); $settings['keyring'] = $this->loadJSONConfigFile('keyring.json'); foreach (\Airship\list_all_files(ROOT . '/config/supplier_keys/', 'json') as $supplier) { $name = \Airship\path_to_filename($supplier, true); $settings['suppliers'][$name] = \Airship\loadJSON($supplier); } $this->lens('admin_settings', ['active_link' => 'bridge-link-admin-settings', 'config' => $settings, 'groups' => $this->acct->getGroupTree()]); }
/** * Create a new page in the current directory * * @param string $cabin * @param string $path * @param array $post * @return mixed */ protected function processNewPage(string $cabin, string $path, array $post = []) : bool { $expected = ['url', 'format', 'page_body', 'save_btn', 'metadata']; if (!\Airship\all_keys_exist($expected, $post)) { return false; } $url = $path . '/' . \str_replace('/', '_', $post['url']); if (!empty($post['ignore_collisions']) && $this->detectCollisions($url, $cabin)) { $this->storeLensVar('post_response', ['message' => \__('The given filename might conflict with another route in this Airship.'), 'status' => 'error']); return false; } $raw = $this->isSuperUser() ? !empty($post['raw']) : false; if ($this->can('publish')) { $publish = $post['save_btn'] === 'publish'; } elseif ($this->can('create')) { $publish = false; } else { $this->storeLensVar('post_response', ['message' => \__('You do not have permission to create new pages.'), 'status' => 'error']); return false; } if ($this->pg->createPage($cabin, $path, $post, $publish, $raw)) { \Airship\redirect($this->airship_cabin_prefix . '/pages/' . $cabin, ['dir' => $path]); } return true; }
/** * Handle short URLs * * @param string $uniqueID * @route b/(.*) */ public function shortURL(string $uniqueID = '') { if (empty($uniqueID)) { \Airship\redirect($this->airship_cabin_prefix . '/blog'); } $long = $this->blog->longURL($uniqueID); \Airship\redirect($this->airship_cabin_prefix . '/blog' . $long); }
/** * If a redirect exists at this path, serve it. * * @param string $uri * @return bool */ public function serveRedirect(string $uri) : bool { $lookup = $this->db->row('SELECT * FROM airship_custom_redirect WHERE oldpath = ?', $uri); if (empty($lookup)) { return false; } if ($lookup['same_cabin']) { // Internal redirects only. Don't create open redirect vulnerabilities. \Airship\redirect(\Airship\LensFunctions\cabin_url($lookup['cabin']) . \trim($lookup['newpath'], '/')); } // Cross-cabin redirects can point to other domains. \Airship\redirect(\rtrim($lookup['newpath'], '/')); return true; }
/** * Reduce code duplication * * @param string $path * @param string $cabin * @return array (array $publicPath, int|null $root) */ protected function loadCommonData(string $path, string $cabin) : array { if (!$this->permCheck()) { \Airship\redirect($this->airship_cabin_prefix); } $root = null; $publicPath = \Airship\chunk($path); $pathInfo = $this->getPath($path); if (!empty($pathInfo)) { try { $root = $this->files->getDirectoryId($pathInfo, $cabin); } catch (FileNotFound $ex) { \Airship\redirect($this->airship_cabin_prefix); } } // return [$pathInfo, $publicPath, $root]; return [$publicPath, $root]; }
/** * Create a new redirect * * @param string $cabin * @route redirects/{string}/new */ public function newRedirect(string $cabin) { $cabins = $this->getCabinNamespaces(); if (!\in_array($cabin, $cabins) && !$this->can('create')) { \Airship\redirect($this->airship_cabin_prefix . '/redirects'); } $this->setTemplateExtraData($cabin); $post = $this->post(new RedirectFilter()); if ($post) { if (\Airship\all_keys_exist(['old_url', 'new_url'], $post)) { if (\preg_match('#^https?://#', $post['new_url'])) { // Less restrictions: $result = $this->pg->createDifferentCabinRedirect(\trim($post['old_url'], '/'), \trim($post['new_url'], '/'), $cabin); } else { $result = $this->pg->createSameCabinRedirect(\trim($post['old_url'], '/'), \trim($post['new_url'], '/'), $cabin); } if ($result) { \Airship\redirect($this->airship_cabin_prefix . '/redirects/' . $cabin); } } } $this->lens('redirect/new', ['cabin' => $cabin]); }
/** * Make sure the secret exists, then get the GoogleAuth object * * @param int $userID * @return GoogleAuth * @throws \Airship\Alerts\Security\UserNotLoggedIn */ protected function twoFactorPreamble(int $userID = 0) : GoogleAuth { if (!$userID) { $userID = $this->getActiveUserId(); } $secret = $this->acct->getTwoFactorSecret($userID); if (empty($secret)) { if (!$this->acct->resetTwoFactorSecret($userID)) { \Airship\json_response(['test2']); \Airship\redirect($this->airship_cabin_prefix); } $secret = $this->acct->getTwoFactorSecret($userID); } return new GoogleAuth($secret, new TOTP(0, (int) ($this->config('two-factor.period') ?? 30), (int) ($this->config('two-factor.length') ?? 6))); }
/** * @route help */ public function helpPage() { if ($this->isLoggedIn()) { $this->storeLensVar('showmenu', true); // $cabins = $this->getCabinNamespaces(); // Get debug information. $helpInfo = ['cabins' => [], 'cabin_names' => \array_values($cabins), 'gears' => [], 'universal' => []]; /** * This might reveal "sensitive" information. By default, it's * locked out of non-administrator users. You can grant access to * other users/groups via the Permissions menu. */ if ($this->can('read')) { $state = State::instance(); if (\is_readable(ROOT . '/config/gadgets.json')) { $helpInfo['universal']['gadgets'] = \Airship\loadJSON(ROOT . '/config/gadgets.json'); } if (\is_readable(ROOT . '/config/content_security_policy.json')) { $helpInfo['universal']['content_security_policy'] = \Airship\loadJSON(ROOT . '/config/content_security_policy.json'); } foreach ($cabins as $cabin) { $cabinData = ['config' => \Airship\loadJSON(ROOT . '/Cabin/' . $cabin . '/manifest.json'), 'content_security_policy' => [], 'gadgets' => [], 'motifs' => [], 'user_motifs' => \Airship\LensFunctions\user_motif($this->getActiveUserId(), $cabin)]; $prefix = ROOT . '/Cabin/' . $cabin . '/config/'; if (\is_readable($prefix . 'gadgets.json')) { $cabinData['gadgets'] = \Airship\loadJSON($prefix . 'gadgets.json'); } if (\is_readable($prefix . 'motifs.json')) { $cabinData['motifs'] = \Airship\loadJSON($prefix . 'motifs.json'); } if (\is_readable($prefix . 'content_security_policy.json')) { $cabinData['content_security_policy'] = \Airship\loadJSON($prefix . 'content_security_policy.json'); } $helpInfo['cabins'][$cabin] = $cabinData; } $helpInfo['gears'] = []; foreach ($state->gears as $gear => $latestGear) { $helpInfo['gears'][$gear] = \Airship\get_ancestors($latestGear); } // Only grab data likely to be pertinent to common issues: $keys = ['airship', 'auto-update', 'debug', 'guzzle', 'notary', 'rate-limiting', 'session_config', 'tor-only', 'twig_cache']; $helpInfo['universal']['config'] = \Airship\keySlice($state->universal, $keys); $helpInfo['php'] = ['halite' => Halite::VERSION, 'libsodium' => ['major' => \Sodium\library_version_major(), 'minor' => \Sodium\library_version_minor(), 'version' => \Sodium\version_string()], 'version' => \PHP_VERSION, 'versionid' => \PHP_VERSION_ID]; } $this->lens('help', ['active_link' => 'bridge-link-help', 'airship' => \AIRSHIP_VERSION, 'helpInfo' => $helpInfo]); } else { // Not a registered user? Go read the docs. No info leaks for you! \Airship\redirect('https://github.com/paragonie/airship-docs'); } }
/** * @route crew/permissions/{string}/context/{id} * * @param string $cabin * @param string $contextId */ public function editContext(string $cabin, string $contextId) { $contextId = (int) $contextId; if (!\in_array($cabin, $this->getCabinNamespaces())) { \Airship\redirect($this->airship_cabin_prefix . '/crew/permissions'); } $context = $this->perms->getContext($contextId, $cabin); if (empty($context)) { \Airship\redirect($this->airship_cabin_prefix . '/crew/permissions' . $cabin); } // Handle post data $post = $this->post(new SaveContextFilter()); if (!empty($post)) { if ($this->perms->saveContext($cabin, $contextId, $post)) { \Airship\redirect($this->airship_cabin_prefix . '/crew/permissions/' . $cabin . '/context/' . $contextId, ['msg' => 'saved']); } } // Okay, $actions = $this->perms->getActionNames($cabin); $groupPerms = $this->perms->buildGroupTree($cabin, $contextId, $actions); $userPerms = $this->perms->buildUserList($cabin, $contextId, $actions); $users = []; foreach ($userPerms as $userid => $userPerm) { $userid = (int) $userid; $users[$userid] = $this->users->getUserAccount($userid, true); unset($users[$userid]['password']); } if (!empty($_GET['msg'])) { if ($_GET['msg'] === 'saved') { $this->storeLensVar('message', \__('Your changes have been saved.')); } } $this->lens('perms/context', ['actions' => $actions, 'cabin' => $cabin, 'context' => $context, 'permissions' => $groupPerms, 'userperms' => $userPerms, 'users' => $users]); }
/** * View a comment * * @param string $commentId * @route blog/comments/view/{id} */ public function viewComment(string $commentId = '') { $commentId = (int) $commentId; $post = $this->post(new CommentFilter()); if (!empty($post)) { switch ($post['comment_btn']) { case 'publish': if ($this->can('publish')) { $this->blog->publishComment($commentId); } break; case 'hide': if ($this->can('publish')) { $this->blog->hideComment($commentId); } break; case 'delete': if ($this->can('delete')) { if ($this->blog->deleteComment($commentId)) { \Airship\redirect($this->airship_cabin_prefix . '/blog/comments'); } } break; } } $this->lens('blog/comments_view', ['active_link' => 'bridge-link-blog-comments', 'comment' => $this->blog->getCommentById((int) $commentId)]); }
/** * This function is called after the dependencies have been injected by * AutoPilot. Think of it as a user-land constructor. */ public function airshipLand() { parent::airshipLand(); if (!$this->isSuperUser()) { \Airship\redirect($this->airship_cabin_prefix); } }