/** * Add the new motif as an option to the Cabin * * @param string $cabin * @return bool */ protected function addMotifToCabin(string $cabin) : bool { if (!\is_dir(ROOT . '/Cabin/' . $cabin)) { return false; } $supplier = $this->supplier->getName(); $name = $this->package; $configFile = ROOT . '/Cabin/' . $cabin . '/config/motifs.json'; $config = \Airship\loadJSON($configFile); $i = 1; while (isset($config[$name])) { $name = $this->package . '-' . ++$i; } $config[$name] = ['enabled' => true, 'supplier' => $supplier, 'name' => $this->package, 'path' => $supplier . '/' . $this->package]; if (!$this->createSymlinks($cabin, $config, $name)) { // This isn't essential. $this->log('Could not create symlinks for Cabin "' . $cabin . '".', LogLevel::WARNING); } return \Airship\saveJSON($configFile, $config); }
/** * First, Content-Security-Policy headers: */ $cspCacheFile = ROOT . '/tmp/cache/csp.' . AutoPilot::$active_cabin . '.json'; if (\file_exists($cspCacheFile) && \filesize($cspCacheFile) > 0) { $csp = CSPBuilder::fromFile($cspCacheFile); } else { $cspfile = ROOT . '/config/Cabin/' . AutoPilot::$active_cabin . '/content_security_policy.json'; if (\file_exists($cspfile)) { $cabinPolicy = \Airship\loadJSON($cspfile); // Merge the cabin-specific policy with the base policy if (!empty($cabinPolicy['inherit'])) { $basePolicy = \Airship\loadJSON(ROOT . '/config/content_security_policy.json'); $cabinPolicy = \Airship\csp_merge($cabinPolicy, $basePolicy); } \Airship\saveJSON($cspCacheFile, $cabinPolicy); $csp = CSPBuilder::fromFile($cspCacheFile); } else { // No cabin policy, use the default $csp = CSPBuilder::fromFile(ROOT . '/config/content_security_policy.json'); } } $state->CSP = $csp; /** * Next, if we're connected over HTTPS, send an HPKP header too: */ if (AutoPilot::isHTTPSConnection()) { $hpkpCacheFile = ROOT . '/tmp/cache/hpkp.' . AutoPilot::$active_cabin . '.json'; if (\file_exists($hpkpCacheFile) && \filesize($hpkpCacheFile) > 0) { $hpkp = HPKPBuilder::fromFile($hpkpCacheFile); $state->HPKP = $hpkp;
/** * Deletes a motif from a cabin's configuration * * @param string $cabin * @param string $key */ protected function deleteMotifFromCabin(string $cabin, string $key) { $filename = ROOT . '/Cabin/' . $cabin . '/config/motifs.json'; $motifs = \Airship\loadJSON($filename); if (isset($motifs[$key])) { unset($motifs[$key]); \Airship\saveJSON($filename, $motifs); } }
/** * Attempt to save the user-provided cabin configuration * * @param string $cabinName * @param array $cabins * @param array $post * @return bool */ protected function saveSettings(string $cabinName, array $cabins = [], array $post = []) : bool { $ds = DIRECTORY_SEPARATOR; $twigEnv = \Airship\configWriter(ROOT . $ds . 'config' . $ds . 'templates'); // Content-Security-Policy $csp = []; foreach ($post['content_security_policy'] as $dir => $rules) { if ($dir === 'upgrade-insecure-requests' || $dir === 'inherit') { continue; } if (empty($rules['allow'])) { $csp[$dir]['allow'] = []; } else { $csp[$dir]['allow'] = []; foreach ($rules['allow'] as $url) { if (!empty($url) && \is_string($url)) { $csp[$dir]['allow'][] = $url; } } } if (isset($rules['disable-security'])) { $csp[$dir]['allow'][] = '*'; } if ($dir === 'script-src') { $csp[$dir]['unsafe-inline'] = !empty($rules['unsafe-inline']); $csp[$dir]['unsafe-eval'] = !empty($rules['unsafe-eval']); $csp[$dir]['self'] = !empty($rules['self']); } elseif ($dir === 'style-src') { $csp[$dir]['unsafe-inline'] = !empty($rules['unsafe-inline']); $csp[$dir]['self'] = !empty($rules['self']); } elseif ($dir !== 'plugin-types') { $csp[$dir]['self'] = !empty($rules['self']); $csp[$dir]['data'] = !empty($rules['data']); } } $csp['inherit'] = !empty($post['content_security_policy']['inherit']); $csp['upgrade-insecure-requests'] = !empty($post['content_security_policy']['upgrade-insecure-requests']); $saveCabins = []; foreach ($cabins as $cab => $cab_data) { if ($cab_data['name'] !== $cabinName) { // Pass-through $saveCabins[$cab] = $cab_data; } else { $saveCabins[$post['config']['path']] = $post['config']; if (isset($cab_data['namespace'])) { // This should be immutable. $saveCabins[$post['config']['path']]['namespace'] = $cab_data['namespace']; } unset($saveCabins['path']); } } // Save CSP \file_put_contents(ROOT . $ds . 'config' . $ds . 'Cabin' . $ds . $cabinName . $ds . 'content_security_policy.json', \json_encode($csp, JSON_PRETTY_PRINT)); // Configuration if (!empty($post['cabin_manage_fallback'])) { $config_extra = \json_decode($post['config_extra'], true); $twig_vars = \json_decode($post['twig_vars'], true); } else { $config_extra = $post['config_extra']; $twig_vars = $post['twig_vars']; } if (!empty($config_extra)) { \Airship\saveJSON(ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'Cabin' . DIRECTORY_SEPARATOR . $cabinName . DIRECTORY_SEPARATOR . 'config.json', $config_extra); } if (!empty($twig_vars)) { \Airship\saveJSON(ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'Cabin' . DIRECTORY_SEPARATOR . $cabinName . DIRECTORY_SEPARATOR . 'twig_vars.json', $twig_vars); } // Clear the cache \unlink(ROOT . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'csp.' . $cabinName . '.json'); if (\extension_loaded('apcu')) { \apcu_clear_cache(); } // Save cabins.json \file_put_contents(ROOT . $ds . 'config' . $ds . 'cabins.json', $twigEnv->render('cabins.twig', ['cabins' => $saveCabins])); // Delete the cabin cache if (\file_exists(ROOT . 'tmp' . $ds . 'cache' . $ds . 'cabin_data.json')) { \unlink(ROOT . 'tmp' . $ds . 'cache' . $ds . 'cabin_data.json'); } return true; }
/** * Update the version identifier stored in the gadgets.json file * * @param UpdateInfo $info * @param array $metaData */ public function updateJSON(UpdateInfo $info, array $metaData = []) { if (!empty($metaData['cabin'])) { $gadgetConfigFile = ROOT . '/Cabin/' . $metaData['cabin'] . '/config/gadgets.json'; } else { $gadgetConfigFile = ROOT . '/config/gadgets.json'; } $gadgetConfig = \Airship\loadJSON($gadgetConfigFile); foreach ($gadgetConfig as $i => $gadget) { if ($gadget['supplier'] === $info->getSupplierName()) { if ($gadget['name'] === $info->getPackageName()) { $gadgetConfig[$i]['version'] = $info->getVersion(); break; } } } \Airship\saveJSON($gadgetConfigFile, $gadgetConfig); }
/** * @param array $motifs * @param array $post * @param string $cabin * @return bool */ protected function updateMotifs(array $motifs, array $post, string $cabin) : bool { foreach ($motifs as $i => $motif) { $motifs[$i]['enabled'] = !empty($post['motifs_enabled']); } return \Airship\saveJSON(ROOT . '/Cabin/' . $cabin . '/config/motifs.json', $motifs); }
/** * Gadget install process. * * 1. Move .phar to the appropriate location. * 2. If this gadget is for a particular cabin, add it to that cabin's * gadgets.json file. * 3. Run the update triggers (install hooks and incremental upgrades). * 4. Clear the cache files. * * @param InstallFile $fileInfo * @return bool */ public function install(InstallFile $fileInfo) : bool { $supplier = $this->supplier->getName(); $fileName = $supplier . '.' . $this->package . '.phar'; $metadata = $this->getMetadata($fileInfo); // Move .phar file to its destination. if (!empty($metadata['cabin'])) { $gadgetConfigFile = ROOT . '/Cabin/' . $metadata['cabin'] . '/config/gadgets.json'; // Cabin-specific gadget $cabin = ROOT . '/Cabin/' . $metadata['cabin'] . '/Gadgets'; if (!\is_dir($cabin)) { $this->log('Could not install; cabin "' . $metadata['cabin'] . '" is not installed.', LogLevel::ERROR); return false; } $filePath = $cabin . '/' . $supplier . '/' . $fileName; if (!\is_dir($cabin . '/' . $supplier)) { \mkdir($cabin . '/' . $supplier, 0775); } } else { $gadgetConfigFile = ROOT . '/config/gadgets.json'; // Universal gadget. (Probably affects the Engine.) $filePath = ROOT . '/Gadgets/' . $supplier . '/' . $fileName; if (!\is_dir(ROOT . '/Gadgets/' . $supplier)) { \mkdir(ROOT . '/Gadgets/' . $supplier, 0775); } } $gadgetConfig = \Airship\loadJSON($gadgetConfigFile); $gadgetConfig[] = [['supplier' => $supplier, 'name' => $this->package, 'version' => $metadata['version'] ?? null, 'path' => $filePath, 'enabled' => true]]; \Airship\saveJSON($gadgetConfigFile, $gadgetConfig); \rename($fileInfo->getPath(), $filePath); // If cabin-specific, add to the cabin's gadget.json if ($metadata['cabin']) { $this->addToCabin($metadata['cabin']); } // Run the update hooks: $alias = 'gadget.' . $fileName; $phar = new \Phar($filePath, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME); $phar->setAlias($alias); // Run the update trigger. if (\file_exists('phar://' . $alias . '/update_trigger.php')) { Sandbox::safeRequire('phar://' . $alias . '/update_trigger.php'); } self::$continuumLogger->store(LogLevel::INFO, 'Install successful', $this->getLogContext($fileInfo)); // Finally, clear the cache files: return $this->clearCache(); }
$link = ROOT . '/public/static/' . $active['name']; if (!\is_link($link)) { // Remove copies, we only allow symlinks in static if (\is_dir($link)) { \rmdir($link); } elseif (\file_exists($link)) { \unlink($link); } // Create a symlink from public/static/* to Cabin/*/public /** @noinspection PhpUsageOfSilenceOperatorInspection */ @\symlink(CABIN_DIR . '/public', ROOT . '/public/static/' . $active['name']); } } // Let's load the default cargo modules if (\is_dir(CABIN_DIR . '/Lens/cargo')) { $cargoCacheFile = ROOT . '/tmp/cache/cargo-' . $active['name'] . '.cache.json'; if (\file_exists($cargoCacheFile)) { $data = Airship\loadJSON($cargoCacheFile); $state->cargo = $data; } else { $dir = \getcwd(); \chdir(CABIN_DIR . '/Lens'); foreach (\Airship\list_all_files('cargo', 'twig') as $cargo) { $idx = \str_replace(['__', '/'], ['', '__'], Binary::safeSubstr($cargo, 6, -5)); Gadgets::loadCargo($idx, $cargo); } \chdir($dir); // Store the cache file \Airship\saveJSON($cargoCacheFile, $state->cargo); } }
// Expose common template snippets to the template loader: $startLink = \implode('/', [ROOT, 'Cabin', $cabinName, 'Lens', 'common']); if (!\is_link($startLink)) { \symlink(ROOT . '/common', $startLink); } } catch (Exception $ex) { $cabin['data'] = null; } if (empty($active_cabin) && $ap::isActiveCabinKey($key)) { if ($cabin['enabled']) { $active_cabin = $key; } else { $cabinDisabled = true; } } $cabins[$key] = $cabin; } if ($cabinDisabled) { \http_response_code(404); echo \file_get_contents(__DIR__ . '/error_pages/no-cabin.html'); exit(1); } if (empty($active_cabin)) { $k = \array_keys($cabins); $active_cabin = \array_pop($k); unset($k); } $state->active_cabin = $active_cabin; $state->cabins = $cabins; \Airship\saveJSON(ROOT . '/tmp/cache/cabin_data.json', ['cabins' => $cabins]); }
if (\is_dir($motifEnd . '/public')) { $motifPublic = CABIN_DIR . '/public/motif/' . $motif; if (!\is_link($motifPublic)) { \symlink($motifEnd . '/public', $motifPublic); } } // Finally, load the configuration: if (\file_exists($motifEnd . '/motif.json')) { $motifConfig['config'] = \Airship\loadJSON($motifEnd . '/motif.json'); } else { $motifConfig['config'] = []; } $motifs[$motif] = $motifConfig; } } \Airship\saveJSON($motifCacheFile, $motifs); $state->motifs = $motifs; } else { die(\__("FATAL ERROR: Motifs file is not readable")); } } } $userMotif = \Airship\LensFunctions\user_motif(); if (!empty($userMotif)) { $activeMotif = $userMotif['name']; } elseif (isset($_settings['active-motif'])) { $activeMotif = $_settings['active-motif']; } if (isset($activeMotif)) { $lens->setBaseTemplate($activeMotif)->loadMotifCargo($activeMotif)->loadMotifConfig($activeMotif)->addGlobal('MOTIF', $state->motif_config); }
/** * Save universal settings * * @param array $post * @return bool */ protected function saveSettings(array $post = []) : bool { $filterName = '\\Airship\\Cabin\\' . CABIN_NAME . '\\AirshipFilter'; if (\class_exists($filterName)) { $filter = new $filterName(); $post = $filter($post); } $twigEnv = \Airship\configWriter(ROOT . '/config/templates'); $csp = []; foreach ($post['content_security_policy'] as $dir => $rules) { if ($dir === 'upgrade-insecure-requests') { continue; } if (empty($rules['allow'])) { $csp[$dir]['allow'] = []; } else { $csp[$dir]['allow'] = []; foreach ($rules['allow'] as $url) { if (!empty($url) && \is_string($url)) { $csp[$dir]['allow'][] = $url; } } } if (isset($rules['disable-security'])) { $csp[$dir]['allow'][] = '*'; } if ($dir === 'script-src') { $csp[$dir]['unsafe-inline'] = !empty($rules['unsafe-inline']); $csp[$dir]['unsafe-eval'] = !empty($rules['unsafe-eval']); } elseif ($dir === 'style-src') { $csp[$dir]['unsafe-inline'] = !empty($rules['unsafe-inline']); } elseif ($dir !== 'plugin-types') { $csp[$dir]['self'] = !empty($rules['self']); $csp[$dir]['data'] = !empty($rules['data']); } } $csp['upgrade-insecure-requests'] = !empty($post['content_security_policy']['upgrade-insecure-requests']); if (isset($csp['inherit'])) { unset($csp['inherit']); } if ($post['universal']['ledger']['driver'] === 'database') { if (empty($post['universal']['ledger']['table'])) { // Table name must be provided. return false; } } // Save CSP \Airship\saveJSON(ROOT . '/config/content_security_policy.json', $csp); if (empty($post['universal']['guest_groups'])) { $post['universal']['guest_groups'] = []; } else { foreach ($post['universal']['guest_groups'] as $i => $g) { $post['universal']['guest_groups'][$i] = (int) $g; } } // Save universal config return \file_put_contents(ROOT . '/config/universal.json', $twigEnv->render('universal.twig', ['universal' => $post['universal']])) !== false; }
/** * We're storing a new public key for this supplier. * * @param Channel $chan * @param TreeUpdate $update * @return void */ protected function revokeKey(Channel $chan, TreeUpdate $update) { $supplier = $update->getSupplier(); $name = $supplier->getName(); $file = ROOT . '/config/supplier_keys/' . $name . '.json'; $supplierData = \Airship\loadJSON($file); foreach ($supplierData['signing_keys'] as $id => $skey) { if (\hash_equals($skey['public_key'], $update->getPublicKeyString())) { // Remove this key unset($supplierData['signing_keys'][$id]); break; } } \Airship\saveJSON($file, $supplierData); \clearstatcache(); // Flush the channel's supplier cache $chan->getSupplier($name, true); }
/** * Update the universal gadgets * * @param array $gadgets * @param array $post * @return bool */ protected function updateUniversalGadgets(array $gadgets, array $post) : bool { $sortedGadgets = []; foreach (\array_unique($post['gadget_order']) as $i => $index) { $gadgets[$index]['enabled'] = !empty($post['gadget_enabled'][$index]); $sortedGadgets[] = $gadgets[$index]; unset($gadgets[$index]); } // Just in case any were omitted foreach ($gadgets as $gadget) { $gadget['enabled'] = false; $sortedGadgets[] = $gadget; } return \Airship\saveJSON(ROOT . '/config/gadgets.json', $sortedGadgets); }
/** * Get the data for the cabins.json file * * This is in a separate method so it can be unit tested * @param \Twig_Environment $twig * @return string */ protected function finalConfigCabins(\Twig_Environment $twig) : string { $cabins = []; foreach ($this->data['cabins'] as $name => $conf) { $cabins[$conf['path']] = ['https' => !empty($conf['https']), 'enabled' => true, 'language' => $conf['lang'] ?? 'en-us', 'canon_url' => $conf['canon_url'], 'name' => $name]; \Airship\saveJSON(ROOT . '/Cabin/' . $name . '/config/config.json', $this->data['config_extra'][$name] ?? []); \Airship\saveJSON(ROOT . '/Cabin/' . $name . '/config/twig_vars.json', $this->data['twig_vars'][$name] ?? []); } return $twig->render('cabins.twig', ['cabins' => $cabins]); }