'feed/history/*.qbk', 'destination' => 'users/history', 'section' => 'history' ), array( 'source' => 'feed/news/*.qbk', 'destination' => 'users/news', 'section' => 'news', ), array( 'source' => 'feed/downloads/*.qbk', 'destination' => 'users/download', 'section' => 'downloads' ), ); var $root; var $hash_file; var $page_cache_file; var $beta_release_notes_file; var $pages = Array(); var $page_cache = Array(); var $beta_release_notes = Array(); var $releases = null; function __construct($root = null, $paths = array()) { if (!$root) { $root = BOOST_WEBSITE_DATA_ROOT_DIR; } $hash_file = BoostWebsite::array_get($paths, 'hash_file', "generated/state/feed-pages.txt"); $page_cache = BoostWebsite::array_get($paths, 'page_cache', "generated/state/page-cache.txt"); $beta_release_notes = BoostWebsite::array_get($paths, 'beta_release_notes', "generated/state/beta_release_notes.txt"); $release_file = BoostWebsite::array_get($paths, 'release_file', "generated/state/release.txt"); $this->root = $root; $this->hash_file = "{$root}/{$hash_file}"; $this->page_cache_file = "{$root}/{$page_cache}";; $this->beta_release_notes_file = "{$root}/{$beta_release_notes}";; $this->releases = new BoostReleases("{$root}/{$release_file}"); if (is_file($this->hash_file)) { foreach(BoostState::load($this->hash_file) as $qbk_file => $record) { if (!isset($record['section'])) { $location_data = $this->get_page_location_data($qbk_file); $record['section'] = $location_data['section']; } $this->pages[$qbk_file] = new BoostPages_Page($qbk_file, $this->get_release_data($qbk_file, $record['section']), $record); } } if (is_file($this->page_cache_file)) { $this->page_cache = BoostState::load($this->page_cache_file); } if (is_file($this->beta_release_notes_file)) { $this->beta_release_notes = BoostState::load($this->beta_release_notes_file); } } function save() { BoostState::save( array_map(function($page) { return $page->state(); }, $this->pages), $this->hash_file); BoostState::save($this->page_cache, $this->page_cache_file); BoostState::save($this->beta_release_notes, $this->beta_release_notes_file); } function reverse_chronological_pages() { $pages = $this->pages; $pub_date_order = array(); $last_published_order = array(); $unpublished_date = new DateTime("+10 years"); foreach($pages as $index => $page) { $pub_date_order[$index] = ($page->release_data ? BoostWebsite::array_get($page->release_data, 'release_date') : null) ?: $page->index_info()->pub_date ?: $unpublished_date; $last_published_order[$index] = $page->last_modified; } array_multisort( $pub_date_order, SORT_DESC, $last_published_order, SORT_DESC, $pages); return $pages; } function scan_for_new_quickbook_pages() { foreach ($this->page_locations as $details) { foreach (glob("{$this->root}/{$details['source']}") as $qbk_file) { assert(strpos($qbk_file, $this->root) === 0); $qbk_file = substr($qbk_file, strlen($this->root) + 1); $this->update_qbk_file($qbk_file, $details['section']); } } } function get_page_location_data($qbk_path) { foreach ($this->page_locations as $details) { if (fnmatch($details['source'], $qbk_path)) { return $details; } } throw new BoostException("Unexpected quickbook file path: {$qbk_path}"); } function get_release_data($qbk_file, $section) { if ($section != 'history' && $section != 'downloads') { return null; } $basename = pathinfo($qbk_file, PATHINFO_FILENAME); if (preg_match('@^([a-z](?:_[a-z]|[a-z0-9])*)_([0-9][0-9_]*)$@i', $basename, $match)) { return $this->releases->get_latest_release_data($match[1], $match[2]); } else { return null; } } function update_qbk_file($qbk_file, $section) { $record = null; if (!isset($this->pages[$qbk_file])) { $release_data = $this->get_release_data($qbk_file, $section); $record = new BoostPages_Page($qbk_file, $release_data); $record->section = $section; $this->pages[$qbk_file] = $record; } else { $record = $this->pages[$qbk_file]; } switch($record->get_release_status()) { case 'released': case null: $qbk_hash = $this->calculate_qbk_hash($record, $section); if ($record->qbk_hash != $qbk_hash && $record->page_state != 'new') { $record->page_state = 'changed'; } break; case 'beta': $qbk_hash = $this->calculate_qbk_hash($record, $section); if ($record->qbk_hash != $qbk_hash && !$record->page_state) { $record->page_state = 'release-data-changed'; } break; case 'dev': // Not building anything for dev entries (TODO: Maybe delete a page // if it exists??? Not sure how that would happen). break; default: // Unknown release status. assert(false); } if ($record->page_state) { $record->section = $section; if ($record->qbk_hash != $qbk_hash) { $record->qbk_hash = $qbk_hash; $record->last_modified = new DateTime(); } } } function calculate_qbk_hash($record, $section) { switch($record->get_release_status()) { case 'beta': // For beta files, don't hash the page source as we don't want to // rebuild when it updates. $context = hash_init('sha256'); hash_update($context, json_encode($this->normalize_release_data( $record->release_data, $record->qbk_file, $section))); return hash_final($context); default: $context = hash_init('sha256'); hash_update($context, json_encode($this->normalize_release_data( $record->release_data, $record->qbk_file, $section))); hash_update($context, str_replace("\r\n", "\n", file_get_contents("{$this->root}/{$record->qbk_file}"))); return hash_final($context); } } // Make the release data look like it used to look in order to get a consistent // hash value. Pretty expensive, but saves constant messing around with hashes. private function normalize_release_data($release_data, $qbk_file, $section) { if (is_null($release_data)) { return null; } // Note that this can be determined from the quickbook file, so if // there's someway that it could change, then either qbk_hash or the // path would change anyway. unset($release_data['release_name']); // Fill in default values. $release_data += array( 'release_notes' => $qbk_file, 'release_status' => 'released', 'version' => '', 'documentation' => null, 'download_page' => null); // Release date wasn't originally included in data for old versions, // and shouldn't be changed, so easiest to ignore it. if (array_key_exists('release_date', $release_data) && ( $section == 'downloads' || !$release_data['version'] || $release_data['version']->compare('1.62.0') <= 0)) { unset($release_data['release_date']); } // Arrange the keys in order. $release_data = $this->arrange_keys($release_data, array( 'release_notes', 'release_status', 'version', 'documentation', 'download_page', 'downloads', 'signature', 'third_party')); // Replace version object with version string. if (array_key_exists('version', $release_data)) { $release_data['version'] = (string) $release_data['version']; } // Turn the downloads and third party downloads into numeric arrays. if (array_key_exists('downloads', $release_data)) { $new_downloads = $this->arrange_keys($release_data['downloads'], array('bz2', 'gz', '7z', 'exe', 'zip')); foreach($new_downloads as &$record) { krsort($record); } unset($record); $release_data['downloads'] = array_values($new_downloads); } if ($release_data && array_key_exists('third_party', $release_data)) { $release_data['third_party'] = array_values($release_data['third_party']); } if ($release_data && array_key_exists('signature', $release_data)) { $release_data['signature'] = $this->arrange_keys($release_data['signature'], array( 'location', 'name', 'key')); } // Normalize fields with special value types. foreach ($release_data as $key => $value) { if (is_object($value)) { if ($value instanceof DateTime || $value instanceof DateTimeInterface) { $release_data[$key] = $value->format(DateTime::ATOM); } else { assert(false); } } } return $release_data; } private function arrange_keys($array, $key_order) { $key_order = array_flip($key_order); $key_sort1 = array(); $key_sort2 = array(); foreach ($array as $key => $data) { $key_sort1[$key] = array_key_exists($key, $key_order) ? $key_order[$key] : 999; $key_sort2[$key] = $key; } array_multisort($key_sort1, SORT_ASC, $key_sort2, SORT_ASC, $array); return $array; } function convert_quickbook_pages($mode = 'update') { $have_quickbook = false; if (BOOST_QUICKBOOK_EXECUTABLE) { try { BoostSuperProject::run_process(BOOST_QUICKBOOK_EXECUTABLE.' --version'); $have_quickbook = true; } catch(ProcessError $e) { echo "Problem running quickbook, will not convert quickbook articles.\n"; } } else { echo "BOOST_QUICKBOOK_EXECUTABLE is empty, will not convert quickbook articles.\n"; } $in_progress_release_notes = array(); $in_progress_failed = false; foreach ($this->pages as $page => $page_data) { if ($mode != 'refresh' && $page_data->dev_data) { $dev_page_data = clone($page_data); $dev_page_data->release_data = $dev_page_data->dev_data; $dev_page_data->page_state = 'changed'; list($boostbook_values, $fresh_cache) = $this->load_quickbook_page($page, $have_quickbook); if (!$boostbook_values) { echo "Unable to generate 'In Progress' entry for {$page}.\n"; $in_progress_failed = true; } else { $in_progress_release_notes[] = array( 'full_title_xml' => $boostbook_values['title_xml'], 'web_date' => 'In Progress', 'download_table' => $dev_page_data->download_table(), 'description_xml' => $this->transform_page_html($dev_page_data, $boostbook_values['description_xhtml']), ); } } if ($mode == 'refresh') { // Refresh: Rebuild pages. $boostbook_values = null; switch ($page_data->get_release_status()) { case 'released': case null: $boostbook_values = BoostWebsite::array_get($this->page_cache, $page); if (!$boostbook_values && $have_quickbook) { $boostbook_values = $this->load_quickbook_page_impl($page); } break; case 'beta': $boostbook_values = BoostWebsite::array_get($this->beta_release_notes, "{$page}:{$page_data->release_data['version']}"); break; case 'dev': break; default: assert(false); } if (!$boostbook_values) { echo "Unable to generate page for {$page}.\n"; } else { $this->generate_quickbook_page($page_data, $boostbook_values); } } else if ($mode == 'update' && $page_data->page_state === 'release-data-changed') { // Release data changed: Update only the release data, // otherwise use existing data. assert($page_data->get_release_status() == 'beta'); $boostbook_values = BoostWebsite::array_get($this->beta_release_notes, "{$page}:{$page_data->release_data['version']}"); if (!$boostbook_values) { echo "No beta cache entry for {$page}.\n"; } else { $this->generate_quickbook_page($page_data, $boostbook_values); $page_data->page_state = null; ++$page_data->update_count; } } else if ($mode == 'update' && $page_data->page_state) { list($boostbook_values, $fresh_cache) = $this->load_quickbook_page($page, $have_quickbook); if (!$boostbook_values) { echo "Unable to generate page for {$page}.\n"; } else { if (!$fresh_cache) { // If we have a dated cache entry, and aren't able to // rebuild it, continue using the current entry, but // don't change the page state - it will try // again on the next run. echo "Using old cached entry for {$page}.\n"; } $this->update_page_data_from_boostbook_values($page_data, $boostbook_values); $this->generate_quickbook_page($page_data, $boostbook_values); if ($fresh_cache) { $page_data->page_state = null; ++$page_data->update_count; if ($page_data->get_release_status() == 'beta') { $this->beta_release_notes["{$page}:{$page_data->release_data['version']}"] = $boostbook_values; } } } } } if ($mode != 'refresh' && !$in_progress_failed) { $template_vars = array( 'releases' => $in_progress_release_notes, ); self::write_template( "{$this->root}/users/history/in_progress.html", __DIR__."/templates/in_progress.php", $template_vars); } } function load_quickbook_page($page, $have_quickbook) { // Hash the quickbook source $hash = hash('sha256', str_replace("\r\n", "\n", file_get_contents("{$this->root}/{$page}"))); // Get the page from quickbook/read from cache $boostbook_values = BoostWebsite::array_get($this->page_cache, $page); $fresh_cache = $boostbook_values && $boostbook_values['hash'] === $hash; if ($have_quickbook && !$fresh_cache) { $boostbook_values = $this->load_quickbook_page_impl($page, $hash); $fresh_cache = true; $this->page_cache[$page] = $boostbook_values; } return array($boostbook_values, $fresh_cache); } function load_quickbook_page_impl($page, $hash = null) { // Hash the quickbook source if (is_null($hash)) { $hash = hash('sha256', str_replace("\r\n", "\n", file_get_contents("{$this->root}/{$page}"))); } $bb_parser = new BoostBookParser(); $xml_filename = tempnam(sys_get_temp_dir(), 'boost-qbk-'); try { echo "Converting ", $page, ":\n"; BoostSuperProject::run_process(BOOST_QUICKBOOK_EXECUTABLE." --output-file {$xml_filename} -I {$this->root}/feed {$this->root}/{$page}"); $values = $bb_parser->parse($xml_filename); $boostbook_values = array( 'hash' => $hash, 'title_xml' => BoostSiteTools::trim_lines($values['title_xhtml']), 'purpose_xml' => BoostSiteTools::trim_lines($values['purpose_xhtml']), 'notice_xml' => BoostSiteTools::trim_lines($values['notice_xhtml']), 'notice_url' => $values['notice_url'], 'pub_date' => $values['pub_date'], 'id' => $values['id'], 'description_xhtml' => BoostSiteTools::trim_lines($values['description_xhtml']), ); } catch (Exception $e) { unlink($xml_filename); throw $e; } unlink($xml_filename); return $boostbook_values; } function update_page_data_from_boostbook_values($page_data, $boostbook_values) { $page_data->load_boostbook_data($boostbook_values); // Set the path where the page should be built. // This can only be done after the quickbook file has been converted, // as the page id is based on the file contents. if (!$page_data->location) { $location_data = $this->get_page_location_data($page_data->qbk_file); $page_data->location = "{$location_data['destination']}/{$page_data->id}.html"; $page_data->guid = "https://www.boost.org/{$page_data->location}"; } } function transform_page_html($page_data, $description_xhtml) { // Transform links in description if (($page_data->get_release_status() === 'dev' || $page_data->get_release_status() === 'beta') && $page_data->get_documentation() ) { $doc_prefix = rtrim($page_data->get_documentation(), '/'); $description_xhtml = BoostSiteTools::transform_links_regex($description_xhtml, '@^(?=/libs/|/doc/html/)@', $doc_prefix); $version = BoostWebsite::array_get($page_data->release_data, 'version'); if ($version && $version->is_numbered_release()) { $final_documentation = "/doc/libs/{$version->final_doc_dir()}"; $description_xhtml = BoostSiteTools::transform_links_regex($description_xhtml, '@^'.preg_quote($final_documentation, '@').'(?=/)@', $doc_prefix); } } return BoostSiteTools::trim_lines($description_xhtml); } function generate_quickbook_page($page_data, $boostbook_values) { $template_vars = array( 'history_style' => '', 'full_title_xml' => $page_data->full_title_xml($boostbook_values['title_xml']), 'title_xml' => $boostbook_values['title_xml'], 'note_xml' => '', 'web_date' => $page_data->web_date($boostbook_values['pub_date']), 'documentation_para' => '', 'download_table' => $page_data->download_table(), 'description_xml' => $this->transform_page_html($page_data, $boostbook_values['description_xhtml']), ); if ($page_data->get_documentation()) { $template_vars['documentation_para'] = '
Documentation';
}
if (strpos($page_data->location, 'users/history/') === 0) {
$template_vars['history_style'] = << List of checksums signed by ".
"".
html_encode($downloads['signature']['name'])."';
switch($this->get_release_status()) {
case 'released':
$output .= '
';
$output .= '* The download links are supported by grants from The C++ Alliance. ';
foreach (array('unix', 'windows') as $platform) {
$platform_downloads = $downloads['downloads'][$platform];
$output .= "\n";
$output .= 'Platform File ';
if ($hash_column) {
$output .= 'SHA256 Hash ';
}
$output .= ' 1) {
$output .= ' rowspan="'.count($platform_downloads).'"';
}
$output .= '>'.html_encode($platform).' ';
$first = true;
foreach ($platform_downloads as $download) {
if (!$first) { $output .= ''; }
$first = false;
$file_name = basename(parse_url($download['url'], PHP_URL_PATH));
$output .= ' ';
}
}
$output .= '';
$output .= html_encode($file_name);
$output .= ' ';
if ($hash_column) {
$output .= '';
$output .= html_encode(BoostWebsite::array_get($download, 'sha256'));
$output .= ' ';
}
$output .= '
';
if (array_key_exists('signature', $downloads)) {
$output .= "Third Party Downloads
\n";
$output .= "\n";
foreach($downloads['third_party'] as $download) {
$output .= '
\n";
}
return $output;
} else if (is_string($downloads)) {
# If the link didn't match the normal version number pattern
# then just use the old fashioned link to sourceforge. */
$output = ' ';
return $output;
}
else {
return '';
}
}
function is_published($state = null) {
if ($this->page_state == 'new') {
return false;
}
if ($this->is_release && !$this->release_data) {
return false;
}
if (!is_null($state) && $this->get_release_status() !== $state) {
return false;
}
return true;
}
function get_release_status() {
switch ($this->section) {
case 'history':
if (!$this->is_release) {
return null;
}
if (!$this->release_data) {
return 'dev';
}
if (array_key_exists('release_status', $this->release_data)) {
return $this->release_data['release_status'];
}
if ($this->release_data['version']->is_numbered_release()) {
return $this->release_data['version']->is_beta() ? 'beta' : 'released';
}
else {
return 'dev';
}
case 'downloads':
return 'released';
default:
return null;
}
}
function get_documentation() {
return is_null($this->release_data) ? null : BoostWebsite::array_get($this->release_data, 'documentation');
}
function get_download_page() {
return is_null($this->release_data) ? null : BoostWebsite::array_get($this->release_data, 'download_page');
}
}