'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'] = << /**/ EOL; } self::write_template( "{$this->root}/{$page_data->location}", __DIR__."/templates/entry.php", $template_vars); } static function write_template($_location, $_template, $_vars) { ob_start(); extract($_vars); include($_template); $r = ob_get_contents(); ob_end_clean(); file_put_contents($_location, $r); } } class BoostPages_Page { // Path to quickbook file var $qbk_file; // Page state var $section, $page_state, $location, $guid, $id, $last_modified; var $qbk_hash, $update_count; // Boostbook data stored for use in indexes etc. // These members should only be used when generating index pages, // otherwise use the boostbook data directly. private $title_xml, $purpose_xml, $notice_xml, $notice_url, $pub_date; // Extra state data that isn't saved. var $is_release = false; // Is this a release? var $release_data = null; // Status of release where appropriate. var $dev_data = null; // Status of release in development. function __construct($qbk_file, $release_data = null, $attrs = array()) { $this->qbk_file = $qbk_file; if ($release_data) { $this->is_release = true; $this->release_data = BoostWebsite::array_get($release_data, 'release'); $this->dev_data = BoostWebsite::array_get($release_data, 'dev'); } $this->section = BoostWebsite::array_get($attrs, 'section'); $this->page_state = BoostWebsite::array_get($attrs, 'page_state'); $this->location = BoostWebsite::array_get($attrs, 'location'); $this->guid = BoostWebsite::array_get($attrs, 'guid'); $this->id = BoostWebsite::array_get($attrs, 'id'); $this->title_xml = BoostWebsite::array_get($attrs, 'title'); $this->purpose_xml = BoostWebsite::array_get($attrs, 'purpose'); $this->notice_xml = BoostWebsite::array_get($attrs, 'notice'); $this->notice_url = BoostWebsite::array_get($attrs, 'notice_url'); $this->last_modified = BoostWebsite::array_get($attrs, 'last_modified'); $this->pub_date = BoostWebsite::array_get($attrs, 'pub_date'); $this->qbk_hash = BoostWebsite::array_get($attrs, 'qbk_hash'); $this->update_count = BoostWebsite::array_get($attrs, 'update_count', 0); // Ensure that pub_date as last_modified are DateTimes. // TODO: Probably not needed any more. if (is_string($this->pub_date)) { $this->pub_date = $this->pub_date == 'In Progress' ? null : new DateTime($this->pub_date); } else if (is_numeric($this->pub_date)) { $this->pub_date = new DateTime("@{$this->pub_date}"); } if (is_string($this->last_modified)) { $this->last_modified = new DateTime($this->last_modified); } else if (is_numeric($this->last_modified)) { $this->last_modified = new DateTime("@{$this->last_modified}"); } if (!$this->guid && $this->location) { $this->guid = "http://www.boost.org/{$this->location}"; } } function state() { return array( 'section' => $this->section, 'page_state' => $this->page_state, 'location' => $this->location, 'guid' => $this->guid, 'id' => $this->id, 'title' => $this->title_xml, 'purpose' => $this->purpose_xml, 'notice' => $this->notice_xml, 'notice_url' => $this->notice_url, 'last_modified' => $this->last_modified, 'pub_date' => $this->pub_date, 'qbk_hash' => $this->qbk_hash, 'update_count' => $this->update_count, ); } function load_boostbook_data($values) { $this->title_xml = $values['title_xml']; $this->purpose_xml = $values['purpose_xml']; $this->notice_xml = $values['notice_xml']; $this->notice_url = $values['notice_url']; $this->pub_date = $values['pub_date']; $this->id = $values['id']; if (!$this->id) { $this->id = strtolower(preg_replace('@[\W]@', '_', $this->title_xml)); } } function index_info() { return (object) array( 'id' => $this->id, 'location' => $this->location, 'guid' => $this->guid, 'full_title_xml' => $this->full_title_xml($this->title_xml), 'web_date' => $this->web_date($this->pub_date), 'download_page' => $this->get_download_page(), 'download_table' => $this->download_table(), 'documentation' => $this->get_documentation(), 'title_xml' => $this->title_xml, 'purpose_xml' => $this->purpose_xml, 'notice_xml' => $this->notice_xml, 'notice_url' => $this->notice_url, 'pub_date' => $this->pub_date, ); } function full_title_xml($title_xml) { switch($this->get_release_status()) { case 'beta': return trim("{$title_xml} beta {$this->release_data['version']->beta_number()}"); case 'dev': return "{$title_xml} - work in progress"; case 'released': case null: return $title_xml; default: assert(false); } } function web_date($pub_date) { $date = null; if (!is_null($this->release_data)) { // For releases, use the release date, not the pub date if (array_key_exists('release_date', $this->release_data)) { $date = $this->release_data['release_date']; } } else { $date = $pub_date; } return $date ? gmdate('F jS, Y H:i', $date->getTimestamp()).' GMT' : 'In Progress'; } function download_table_data() { if (is_null($this->release_data)) { return null; } $downloads = BoostWebsite::array_get($this->release_data, 'downloads'); $signature = BoostWebsite::array_get($this->release_data, 'signature'); $third_party = BoostWebsite::array_get($this->release_data, 'third_party'); if (!$downloads && !$third_party) { return $this->get_download_page(); } $tabled_downloads = array(); foreach ($downloads as $download) { // Q: Good default here? $line_endings = BoostWebsite::array_get($download, 'line_endings', 'unix'); unset($download['line_endings']); $tabled_downloads[$line_endings][] = $download; } $result = array( 'downloads' => $tabled_downloads ); if ($signature) { $result['signature'] = $signature; } if ($third_party) { $result['third_party'] = $third_party; } return $result; } function download_table() { $downloads = $this->download_table_data(); if (is_array($downloads)) { # Print the download table. $hash_column = false; foreach($downloads['downloads'] as $x) { foreach($x as $y) { if (array_key_exists('sha256', $y)) { $hash_column = true; } } } $output = ''; $output .= ' '; switch($this->get_release_status()) { case 'released': $output .= ''; break; case 'beta': $output .= ''; break; case 'dev': $output .= ''; break; default: assert(false); } $output .= ''; if ($hash_column) { $output .= ''; } $output .= ''; foreach (array('unix', 'windows') as $platform) { $platform_downloads = $downloads['downloads'][$platform]; $output .= "\n"; $output .= ''; $first = true; foreach ($platform_downloads as $download) { if (!$first) { $output .= ''; } $first = false; $file_name = basename(parse_url($download['url'], PHP_URL_PATH)); $output .= ''; if ($hash_column) { $output .= ''; } $output .= ''; } } $output .= '
DownloadsBeta DownloadsDevelopment Downloads
PlatformFileSHA256 Hash
1) { $output .= ' rowspan="'.count($platform_downloads).'"'; } $output .= '>'.html_encode($platform).'
'; $output .= html_encode($file_name); $output .= ''; $output .= html_encode(BoostWebsite::array_get($download, 'sha256')); $output .= '
'; $output .= '* The download links are supported by grants from The C++ Alliance.

'; if (array_key_exists('signature', $downloads)) { $output .= "

List of checksums signed by ". "". html_encode($downloads['signature']['name'])."

\n"; } if (array_key_exists('third_party', $downloads)) { $output .= "\n"; $output .= "

Third Party Downloads

\n"; $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 = '

'; switch($this->get_release_status()) { case 'released': $output .= 'Download this release.'; break; case 'beta': $output .= 'Download this beta release.'; break; case 'dev': $output .= 'Download snapshot.'; break; default: assert(false); } $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'); } }