/** * @route motif-config/{string} * * @param string $motifName */ public function configure(string $motifName) { $motifs = $this->getAllMotifs(); if (!\array_key_exists($motifName, $motifs)) { \Airship\redirect($this->airship_cabin_prefix . '/motifs'); } if (!$this->can('update')) { \Airship\redirect($this->airship_cabin_prefix . '/motifs'); } $selected = $motifs[$motifName]; $path = ROOT . '/Motifs/' . $selected['supplier'] . '/' . $selected['name']; // Should we load overload the configuration lens? if (\file_exists($path . '/lens/config.twig')) { Gadgets::loadCargo('bridge_motifs_config_overloaded', 'motif/' . $motifName . '/config.twig'); } $inputFilter = null; if (\file_exists($path . '/config_filter.php')) { $inputFilter = $this->getConfigFilter($path . '/config_filter.php'); } try { $motifConfig = \Airship\loadJSON(ROOT . '/config/motifs/' . $motifName . '.json'); } catch (\Throwable $ex) { $motifConfig = []; } // Handle POST data if ($inputFilter instanceof InputFilterContainer) { $post = $this->post($inputFilter); } else { $post = $this->post(); if (\is_string($post['motif_config'])) { $post['motif_config'] = \Airship\parseJSON($post['motif_config'], true); } } if ($post) { if (empty($post['motif_config'])) { $post['motif_config'] = []; } if ($this->saveMotifConfig($motifName, $post['motif_config'])) { \Airship\redirect($this->airship_cabin_prefix . '/motif_config/' . $motifName); } } $this->lens('motif_configure', ['cabin_name' => $motifName, 'motifs' => $motifs, 'motif_config' => $motifConfig, 'title' => \__('Configuring %s/%s', 'default', Util::noHTML($selected['supplier']), Util::noHTML($selected['name']))]); }
/** * Was the checksum of this update stored in Keyggdrasil? * * Dear future security auditors: This is important. * * @param UpdateInfo $info * @param UpdateFile $file * @return bool */ public function checkKeyggdrasil(UpdateInfo $info, UpdateFile $file) : bool { $debugArgs = ['supplier' => $info->getSupplierName(), 'name' => $info->getPackageName(), 'root' => $info->getMerkleRoot()]; $this->log('Checking Keyggdrasil', LogLevel::DEBUG, $debugArgs); $db = \Airship\get_database(); $merkle = $db->row('SELECT * FROM airship_tree_updates WHERE merkleroot = ?', $info->getMerkleRoot()); if (empty($merkle)) { $this->log('Merkle root not found in tree', LogLevel::DEBUG, $debugArgs); // Not found in Keyggdrasil return false; } $data = \Airship\parseJSON($merkle['data'], true); if ($data['action'] !== 'CORE') { if (!\hash_equals($this->type, $data['pkg_type'])) { $this->log('Wrong package type', LogLevel::DEBUG, $debugArgs); // Wrong package type return false; } if (!\hash_equals($info->getSupplierName(), $data['supplier'])) { $this->log('Wrong supplier', LogLevel::DEBUG, $debugArgs); // Wrong supplier return false; } if (!\hash_equals($info->getPackageName(), $data['name'])) { $this->log('Wrong package', LogLevel::DEBUG, $debugArgs); // Wrong package return false; } } $data = \Airship\parseJSON($merkle['data'], true); // Finally, we verify that the checksum matches the entry in our Merkle tree: return \hash_equals($file->getHash(), $data['checksum']); }
/** * Load a JSON file and parses it * * @param string $file - The absolute path of the file name * @return mixed * @throws AccessDenied * @throws FileNotFound */ function loadJSON(string $file) { // Very specific checks if (!\file_exists($file)) { throw new FileNotFound($file); } if (!\is_readable($file)) { throw new AccessDenied($file); } // The meat of this function is kind of boring: return \Airship\parseJSON(\file_get_contents($file), true); }
/** * Verifies that the Merkle root exists, matches this package and version, * and has the same checksum as the one we calculated. * * @param InstallFile $file * @return bool */ public function verifyMerkleRoot(InstallFile $file) : bool { $debugArgs = ['supplier' => $this->supplier->getName(), 'name' => $this->package]; $db = \Airship\get_database(); $merkle = $db->row('SELECT * FROM airship_tree_updates WHERE merkleroot = ?', $file->getMerkleRoot()); if (empty($merkle)) { $this->log('Merkle root not found in tree', LogLevel::DEBUG, $debugArgs); // Not found in Keyggdrasil return false; } $data = \Airship\parseJSON($merkle['data'], true); $instType = \strtolower($this->type); $keyggdrasilType = \strtolower($data['pkg_type']); if (!\hash_equals($instType, $keyggdrasilType)) { $this->log('Wrong package type', LogLevel::DEBUG, $debugArgs); // Wrong package type return false; } if (!\hash_equals($this->supplier->getName(), $data['supplier'])) { $this->log('Wrong supplier', LogLevel::DEBUG, $debugArgs); // Wrong supplier return false; } if (!\hash_equals($this->package, $data['name'])) { $this->log('Wrong package', LogLevel::DEBUG, $debugArgs); // Wrong package return false; } // Finally, we verify that the checksum matches the entry in our Merkle tree: return \hash_equals($file->getHash(), $data['checksum']); }
/** * @covers \Airship\parseJSON() */ public function testParseJSON() { $this->assertEquals((object) ['a' => true], \Airship\parseJSON('{"a":true}')); $this->assertSame(['a' => true], \Airship\parseJSON('{"a":true}', true)); $this->assertSame(['a' => true], \Airship\parseJSON('{"a":true/*, "b": false */}', true)); $this->assertSame(['a' => true], \Airship\parseJSON(' { "a":true /*, "b": false */ }', true)); $this->assertSame(['a' => true], \Airship\parseJSON(' { "a":true //, "b": false }', true)); $this->assertSame(['a' => true, 'b' => false], \Airship\parseJSON(' { "a":true, "b": false //, "c": false }', true)); }
/** * Get the user's selected Motif * * @param int|null $userId * @param string $cabin * @return array */ function user_motif(int $userId = null, string $cabin = \CABIN_NAME) : array { static $userCache = []; $state = State::instance(); if (\count($state->motifs) === 0) { return []; } if (empty($userId)) { $userId = \Airship\LensFunctions\userid(); if (empty($userId)) { $k = \array_keys($state->motifs)[0]; return $state->motifs[$k] ?? []; } } // Did we cache these preferences? if (isset($userCache[$userId])) { return $state->motifs[$userCache[$userId]]; } $db = \Airship\get_database(); $userPrefs = $db->cell('SELECT preferences FROM airship_user_preferences WHERE userid = ?', $userId); if (empty($userPrefs)) { // Default $k = \array_keys($state->motifs)[0]; $userCache[$userId] = $k; return $state->motifs[$k] ?? []; } $userPrefs = \Airship\parseJSON($userPrefs, true); if (isset($userPrefs['motif'][$cabin])) { $split = \explode('/', $userPrefs['motif'][$cabin]); foreach ($state->motifs as $k => $motif) { if (empty($motif['config'])) { continue; } if ($motif['supplier'] === $split[0] && $motif['name'] === $split[1]) { // We've found a match: $userCache[$userId] = $k; return $state->motifs[$k]; } } } // When all else fails, go with the first one $k = \array_keys($state->motifs)[0]; $userCache[$userId] = $k; return $state->motifs[$k] ?? []; }
/** * Perform a POST request, get a decoded JSON response. * * @param string $url * @param array $params * @return mixed */ public function postJSON(string $url, array $params = []) { return \Airship\parseJSON($this->postReturnBody($url, $params), true); }
/** * Motif install process. * * 1. Extract files to the appropriate directory. * 2. If this is a cabin-specific motif, update motifs.json. * Otherwise, it's a global Motif. Enable for all cabins. * 3. Create symbolic links. * 4. Clear cache files. * * @param InstallFile $fileInfo * @return bool */ public function install(InstallFile $fileInfo) : bool { $path = $fileInfo->getPath(); $zip = new \ZipArchive(); $res = $zip->open($path); if ($res !== true) { $this->log('Could not open the ZipArchive.', LogLevel::ERROR); return false; } // Extraction destination directory $dir = \implode(DIRECTORY_SEPARATOR, [ROOT, 'Motifs', $this->supplier->getName(), $this->package]); if (!\is_dir($dir)) { \mkdir($dir, 0775, true); } // Grab metadata $metadata = \Airship\parseJSON($zip->getArchiveComment(\ZipArchive::FL_UNCHANGED), true); if (isset($metadata['cabin'])) { $cabin = $this->expandCabinName($metadata['cabin']); if (!\is_dir(ROOT . '/Cabin/' . $cabin)) { $this->log('Could not install; cabin "' . $cabin . '" is not installed.', LogLevel::ERROR); return false; } } else { $cabin = null; } // Extract the new files to the current directory if (!$zip->extractTo($dir)) { $this->log('Could not extract Motif to its destination.', LogLevel::ERROR); return false; } // Add to the relevant motifs.json files if ($cabin) { $this->addMotifToCabin($cabin); } else { foreach (\glob(ROOT . '/Cabin/') as $cabin) { if (\is_dir($cabin)) { $this->addMotifToCabin(\Airship\path_to_filename($cabin)); } } } self::$continuumLogger->store(LogLevel::INFO, 'Motif install successful', $this->getLogContext($fileInfo)); // Finally, nuke the cache: return $this->clearCache(); }