/** * Hook function which * - Sets the version correctly when editing a topic * - Redirects to the first topic in a manual if the user requested a bare manual URL * - Redirect to the landing page when there are no available versions */ public function onArticleFromTitleQuickLookup(&$title, &$article) { global $wgScriptPath; if (preg_match('/&action=edit/', $_SERVER['PATH_INFO'])) { // Check referrer and see if we're coming from a doc page. // If so, we're editing it, so we should force the version // to be from the referrer. if (preg_match('/^' . str_replace("/", "\\/", $wgScriptPath) . '\\/' . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME . '\\/(\\w+)\\/((latest|[\\w\\.]*)\\/)?(\\w+)\\/?/i', $_SERVER['HTTP_REFERER'], $match)) { $targetProduct = $match[1]; $targetVersion = $match[3]; if ($targetVersion == "latest") { PonyDocsProductVersion::SetSelectedVersion($targetProduct, PonyDocsProductVersion::GetLatestReleasedVersion($targetProduct)->getVersionName()); } else { PonyDocsProductVersion::SetSelectedVersion($targetProduct, $targetVersion); } } } // Match a URL like /Documentation/PRODUCT if (preg_match('/^' . str_replace("/", "\\/", $wgScriptPath) . '\\/' . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME . '\\/([' . PONYDOCS_PRODUCT_LEGALCHARS . ']+)$/i', $_SERVER['PATH_INFO'], $match)) { $targetProduct = $match[1]; $version = PonyDocsProductVersion::GetVersions($targetProduct, TRUE); //check for product not found if (empty($version)) { PonyDocsExtension::redirectToLandingPage(); return true; } } // Matches a URL like /Documentation/PRODUCT/VERSION/MANUAL // TODO: Should match PONYDOCS_PRODUCTMANUAL_LEGALCHARS instead of \w at the end if (preg_match('/^' . str_replace("/", "\\/", $wgScriptPath) . '\\/' . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME . '\\/([' . PONYDOCS_PRODUCT_LEGALCHARS . ']+)\\/([' . PONYDOCS_PRODUCTVERSION_LEGALCHARS . ']+)\\/(\\w+)\\/?$/i', $_SERVER['PATH_INFO'], $match)) { $targetProduct = $match[1]; $targetVersion = $match[2]; $targetManual = $match[3]; $p = PonyDocsProduct::GetProductByShortName($targetProduct); if (!$p instanceof PonyDocsProduct) { $wgHooks['BeforePageDisplay'][] = "PonyDocsExtension::handle404"; return false; } // User wants to find first topic in a requested manual. // Load up versions PonyDocsProductVersion::LoadVersionsForProduct($targetProduct); // Determine version if ($targetVersion == '') { // No version specified, use the user's selected version $ver = PonyDocsProductVersion::GetVersionByName($targetProduct, PonyDocsProductVersion::GetSelectedVersion($targetProduct)); } else { if (strtolower($targetVersion) == "latest") { // User wants the latest version. $ver = PonyDocsProductVersion::GetLatestReleasedVersion($targetProduct); } else { // Okay, they want to get a version by a specific name $ver = PonyDocsProductVersion::GetVersionByName($targetProduct, $targetVersion); } } if (!$ver) { if (PONYDOCS_DEBUG) { error_log("DEBUG [" . __METHOD__ . ":" . __LINE__ . "] redirecting to {$wgScriptPath}/" . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME); } header('Location: ' . $wgScriptPath . '/' . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME); die; } // Okay, the version is valid, let's set the user's version. PonyDocsProductVersion::SetSelectedVersion($targetProduct, $ver->getVersionName()); PonyDocsProductManual::LoadManualsForProduct($targetProduct); $manual = PonyDocsProductManual::GetManualByShortName($targetProduct, $targetManual); if (!$manual) { // Rewrite to Main documentation if (PONYDOCS_DEBUG) { error_log("DEBUG [" . __METHOD__ . ":" . __LINE__ . "] redirecting to {$wgScriptPath}/" . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME); } header('Location: ' . $wgScriptPath . '/' . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME); die; } elseif (!$manual->isStatic()) { // Get the TOC out of here! heehee $toc = new PonyDocsTOC($manual, $ver, $p); list($toc, $prev, $next, $start) = $toc->loadContent(); //Added empty check for WEB-10038 if (empty($toc)) { PonyDocsExtension::redirectToLandingPage(); return FALSE; } foreach ($toc as $entry) { if (isset($entry['link']) && $entry['link'] != "") { // We found the first article in the manual with a link. // Redirect to it. if (PONYDOCS_DEBUG) { error_log("DEBUG [" . __METHOD__ . ":" . __LINE__ . "] redirecting to " . $entry['link']); } header("Location: " . $entry['link']); die; } } //Replace die with a warning log and redirect error_log("WARNING [" . __METHOD__ . ":" . __LINE__ . "] redirecting to " . PonyDocsExtension::getDefaultUrl()); PonyDocsExtension::redirectToLandingPage(); return FALSE; } } return TRUE; }
/** * This is called upon loading the special page. It should write output to * the page with $wgOut */ public function execute() { global $wgOut, $wgArticlePath, $wgScriptPath, $wgUser; global $wgRequest; global $wgDBprefix; $currentProduct = ''; $currentVersion = ''; $collapseAll = FALSE; $dbr = wfGetDB(DB_SLAVE); // Set headers and title of the page, get value of "t" from GET/POST $this->setHeaders(); $title = $wgRequest->getVal('t'); if (empty($title)) { $wgOut->setPagetitle("Documentation Linkage"); $wgOut->addHTML('No topic specified.'); return; } $wgOut->setPagetitle("Documentation Linkage For " . $title); // Parse "t" (the title we're looking for inbound links to) // Find titles for all inherited versions, etc. $titlePieces = explode(':', $title); $plainTitle = $title; // Create a new Title from text, such as what one would find in a link. Decodes any HTML entities in the text. $title = Title::newFromText($title); $toUrls = array(); // Do PonyDocs-specific stuff (loop through all inherited versions) if ($titlePieces[0] == PONYDOCS_DOCUMENTATION_NAMESPACE_NAME) { // Get all the versions in the product $versions = PonyDocsProductVersion::LoadVersionsForProduct($titlePieces[1], true); if (empty($versions)) { error_log('WARNING [PonyDocs] [' . __CLASS__ . '] Unable to find product versions for this topic: ' . $title); } $currentProduct = $titlePieces[1]; $currentVersion = $titlePieces[4]; // Get the latest released version of this product $latestVersionObj = PonyDocsProductVersion::GetLatestReleasedVersion($titlePieces[1]); if (is_object($latestVersionObj)) { $latestVersion = $latestVersionObj->getVersionName(); } else { error_log('WARNING [PonyDocs] [' . __CLASS__ . '] Unable to find latest released version of ' . $titlePieces[1]); } // Generate a title without the version so we can dynamically generate a list of titles with all inherited versions $titleNoVersion = $titlePieces[0] . ":" . $titlePieces[1] . ":" . $titlePieces[2] . ":" . $titlePieces[3]; // Search the database for matching to_links for each inherited version if (is_array($versions)) { foreach ($versions as $ver) { // Add this URL to array of URLs to search db for $toUrls[] = PonyDocsExtension::translateTopicTitleForDocLinks($titleNoVersion, NULL, $ver); // Compare this version with latest version. If they're the same, add the URL with "latest" too. $thisVersion = $ver->getVersionName(); if ($thisVersion == $latestVersion) { $titleLatestVersion = $titlePieces[0] . ':' . $titlePieces[1] . ':' . $titlePieces[2] . ':' . $titlePieces[3] . ':latest'; $toUrls[] = PonyDocsExtension::translateTopicTitleForDocLinks($titleLatestVersion); } } } else { error_log('WARNING [PonyDocs] [' . __CLASS__ . '] Unable to find versions for ' . $title); } } else { // Do generic mediawiki stuff for non-PonyDocs namespaces $collapseAll = TRUE; $toUrls[] = PonyDocsExtension::translateTopicTitleForDocLinks($title); } // Query the database for the list of toUrls we've collated if (!empty($toUrls)) { foreach ($toUrls as &$toUrl) { $toUrl = $dbr->strencode($toUrl); } $inUrls = "'" . implode("','", $toUrls) . "'"; $query = "SELECT * FROM " . $wgDBprefix . "ponydocs_doclinks WHERE to_link IN ({$inUrls})"; $results = $dbr->query($query); } // Create array of links, sorted by product and version $links = array(); // Loop through results and save into handy dandy links array if (!empty($results)) { foreach ($results as $result) { $fromProduct = ''; $fromVersion = ''; $displayUrl = ''; if (strpos($result->from_link, PONYDOCS_DOCUMENTATION_NAMESPACE_NAME) !== false) { // If this is a PonyDocs style links, with slashes, // save product, version, display URL accordingly. $pieces = explode('/', $result->from_link); $fromProduct = $pieces[1]; $fromVersion = $pieces[2]; $displayUrl = $result->from_link; } else { // If this is a generic mediawiki style link, with colons (or not), // set product to the namespace, and remove namespace // from the display URL. Leave version blank. if (strpos($result->from_link, ':') !== false) { $pieces = explode(':', $result->from_link); $fromProduct = $pieces[0]; // The "product" will be the namespace $displayUrl = $pieces[1]; // So the namespace doesn't show in every URL } else { // it's possible to have a link with no colons $fromProduct = 'Other'; // No namespace, so the "product" will be the string "Other" $displayUrl = $result->from_link; } $fromVersion = 'None'; // No concept of versions outside of PonyDocs } // Put all this stuff in an array that we can use to generate HTML $links[$fromProduct][$fromVersion][] = array('from_link' => $result->from_link, 'to_link' => $result->to_link, 'display_url' => $displayUrl); } } // Make HTML go! ob_start(); ?> <div class="doclinks"> <h2>Inbound links to <?php echo $plainTitle; ?> from other topics.</h2> <?php // If there are no links, display a message saying as much if (empty($links)) { ?> <p>No links to <?php echo $plainTitle; ?> (and its inherited versions) from other topics.</p> <?php } else { // Display all links, ordered by product then version foreach ($links as $fromProduct => $fromVersions) { // If this is a PonyDocs Product if (PonyDocsProduct::IsProduct($fromProduct)) { // Get versions for this product, so we can display the versions in the correct order PonyDocsProductVersion::LoadVersionsForProduct($fromProduct, true); $fromProductVersions = PonyDocsProductVersion::GetVersions($fromProduct); // If there are no valid versions for this product/user, then skip the product name header. if (!count($fromProductVersions)) { continue; } ?> <h2><?php echo $fromProduct; ?> </h2> <?php foreach ($fromProductVersions as $fromProductVersionObj) { $fromProductVersionName = $fromProductVersionObj->getVersionName(); // If there are doclinks from this version, print them if (array_key_exists($fromProductVersionName, $fromVersions)) { // Expand containers of incoming links from the current Product and Version // Expand containers of incoming links from other Products // But don't expand any containers if this is not a PonyDocs product $selected = ''; if (($currentProduct != $fromProduct || $currentVersion == $fromProductVersionName) && !$collapseAll) { $selected = 'selected'; } ?> <h3 class="doclinks-collapsible <?php print $selected; ?> "> <?php echo $fromProduct . ' ' . $fromProductVersionName; ?> </h3> <ul> <?php foreach ($fromVersions[$fromProductVersionName] as $linkAry) { ?> <li> <a href="<?php echo str_replace('$1', $linkAry['from_link'], $wgArticlePath); ?> "> <?php echo $linkAry['display_url']; ?> </a> </li> <?php } ?> </ul> <?php } } } else { ?> <h2><?php echo $fromProduct; ?> </h2> <h3 class="doclinks-collapsible selected">Latest</h3> <?php // This is not a PonyDocs product, don't worry about sorting foreach ($fromVersions as $fromVersion => $fromVersionData) { ?> <ul> <?php foreach ($fromVersionData as $linkAry) { ?> <li> <a href="<?php echo str_replace('$1', $linkAry['from_link'], $wgArticlePath); ?> "> <?php echo $linkAry['display_url']; ?> </a> </li> <?php } ?> </ul> <?php } } } } ?> </div> <?php $htmlContent = ob_get_contents(); ob_end_clean(); $wgOut->addHTML($htmlContent); return true; }
/** * Parse the content of the TOC management page. * * It should be loaded and stored and this sort of breaks the design * in the way that it returns the template ready array of data which PonyDocsWiki is really supposed to be returning, * but I do not see the point or use of an intermediate format other than to bloat the code. * * It returns an array of arrays, which can be stored as a single array or separated using the list() = loadContnet() syntax. * * toc: This is the actual TOC as a list of arrays, * each array having a set of keys available to specify the TOC level, text, href, etc. * prev: Assoc array containing the 'previous' link data (text, href), or empty if there isn't one. * next: Assoc array containing the 'next' link data or empty if there isn't one. * start: Assoc array containing the data for the FIRST topic in the TOC. * * These can be captured in a variable when calling and individually accessed or captured using the list() construct * i.e.: list( $toc, $prev, $next, $start ) = $toc->loadContent(). * * @FIXME: Store results internally and then have a $reload flag as param. * $content = $toc- * * @return array */ public function loadContent() { global $wgArticlePath; global $wgTitle; global $wgScriptPath; global $wgPonyDocs; global $title; /** * From this we have the page ID of the TOC page to use -- fetch it then parse it so we can produce an output TOC array. * * This array will contain one array per item with the following keys: * - 'level': 0= Arbitary Section Name, 1= Actual topic link. * - 'link': Link (wiki path) to item; may be unset for section headers (or set to first section H1)? * - 'text': Text to show in sidebar TOC. * - 'current': 1 if this is the currently selected topic, 0 otherwise. * * We also have to store the index of the current section in our loop. * * The reason for this is so that we can remove any sections which have no defined/valid topics listed. * * This will also assist in our prev/next links which are stored in special indices. */ // Our title is our url. // We should check to see if latest is our version. // If so, we want to FORCE the URL to include /latest/ as the version instead of the version that the user is currently in $tempParts = explode("/", $title); $latest = FALSE; if (isset($tempParts[1]) && !strcmp($tempParts[1], "latest")) { $latest = TRUE; } $selectedProduct = $this->pProduct->getShortName(); $selectedVersion = $this->pInitialVersion->getVersionName(); $selectedManual = $this->pManual->getShortName(); // Okay, let's determine if the VERSION that the user is in is latest, if so, we should set latest to true. if (PonyDocsProductVersion::GetLatestReleasedVersion($selectedProduct) != NULL) { if ($selectedVersion == PonyDocsProductVersion::GetLatestReleasedVersion($selectedProduct)->getVersionName()) { $latest = TRUE; } } $cache = PonyDocsCache::getInstance(); $key = "TOCCACHE-" . $selectedProduct . "-" . $selectedManual . "-" . $selectedVersion; $toc = $cache->get($key); // Cache did not exist, let's load our content is build up our cache entry. if ($toc === NULL && is_object($this->pTOCArticle) && is_a($this->pTOCArticle, 'Article')) { // The current index of the element in $toc we will work on $idx = 0; $section = -1; $content = $this->pTOCArticle->getContent(); $lines = explode("\n", $content); foreach ($lines as $line) { /** * Indicates an arbitrary section header if it does not begin with a bullet point. * This is level 0 in our TOC and is not a link of any type (?). */ if (!isset($line[0]) || $line[0] != '*') { /** * See if we are CLOSING a section (i.e. $section != -1). If so, check 'subs' and ensure its >0, * otherwise we need to remove the section from the list. */ if ($section != -1 && !$toc[$section]['subs']) { unset($toc[$section]); } if (isset($line[0]) && ctype_alnum($line[0])) { $toc[$idx] = array('level' => 0, 'subs' => 0, 'link' => '', 'text' => $line, 'current' => FALSE); $section = $idx; } /** * This is a bullet point and thus an actual topic which can be linked to in MediaWiki. * {{#topic:H1 Of Topic Page}} */ } else { if (-1 == $section) { continue; } $topicRegex = '/' . PonyDocsTopic::getTopicRegex() . '/i'; if (!preg_match($topicRegex, $line, $matches)) { continue; } $baseTopic = $matches[1]; $title_suffix = preg_replace('/([^' . str_replace(' ', '', Title::legalChars()) . '])/', '', $baseTopic); $title = PONYDOCS_DOCUMENTATION_NAMESPACE_NAME . ":{$selectedProduct}:{$selectedManual}:{$title_suffix}"; $newTitle = PonyDocsTopic::GetTopicNameFromBaseAndVersion($title, $selectedProduct); /** * Hide topics which have no content (i.e. have not been created yet) from the user viewing. * * Authors must go to the TOC page in order to view and edit these. * * The only way to do this (the cleanest/quickest) is to create a Title object then see if its article ID is 0 * * @tbd: Fix so that the section name is hidden if no topics are visible? */ $t = Title::newFromText($newTitle); if (!$t || !$t->getArticleID()) { continue; } /** * Obtain H1 content from the article -- WE NEED TO CACHE THIS! */ $h1 = PonyDocsTopic::FindH1ForTitle($newTitle); if ($h1 === FALSE) { $h1 = $newTitle; } $href = str_replace('$1', PONYDOCS_DOCUMENTATION_NAMESPACE_NAME . "/{$selectedProduct}/{$selectedVersion}/{$selectedManual}/{$title_suffix}", $wgArticlePath); $toc[$idx] = array('level' => 1, 'page_id' => $t->getArticleID(), 'link' => $href, 'toctitle' => $baseTopic, 'text' => $h1, 'section' => $toc[$section]['text'], 'title' => $newTitle, 'class' => 'toclevel-1'); $toc[$section]['subs']++; } $idx++; } if (!$toc[$section]['subs']) { unset($toc[$section]); } // Okay, let's store in our cache. $cache->put($key, $toc, TOC_CACHE_TTL, TOC_CACHE_TTL / 4); } if ($toc) { $currentIndex = -1; $start = array(); // Go through and determine start, prev, next and current elements. foreach ($toc as $idx => &$entry) { // Not using $entry. Only interested in $idx. // This allows us to process tocs with removed key indexes. if ($toc[$idx]['level'] == 1) { if (empty($start)) { $start = $toc[$idx]; } // Determine current $toc[$idx]['current'] = strcmp($wgTitle->getPrefixedText(), $toc[$idx]['title']) ? FALSE : TRUE; if ($toc[$idx]['current']) { $currentIndex = $idx; } // Now rewrite link with latest, if we are in latest if ($latest) { $safeVersion = preg_quote($selectedVersion, '#'); // Lets be specific and replace the version and not some other part of the URI that might match... $toc[$idx]['link'] = preg_replace('#^/' . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME . '/([' . PONYDOCS_PRODUCT_LEGALCHARS . ']+)/' . "{$safeVersion}#", '/' . PONYDOCS_DOCUMENTATION_NAMESPACE_NAME . '/$1/latest', $toc[$idx]['link'], 1); } } } /** * Figure out previous and next links. * * Previous should point to previous topic regardless of section, so our best bet is to skip any 'level=0'. * * Next works the same way. */ $prev = $next = $idx = -1; if ($currentIndex >= 0) { $idx = $currentIndex; while ($idx >= 0) { --$idx; if (isset($toc[$idx]) && $toc[$idx]['level'] == 1) { $prev = $idx; break; } } $idx = $currentIndex; // Array is sparse, so sizeof() truncates the end. Use max key instead. while ($idx <= max(array_keys($toc))) { ++$idx; if (isset($toc[$idx]) && $toc[$idx]['level'] == 1) { $next = $idx; break; } } if ($prev != -1) { $prev = array('link' => $toc[$prev]['link'], 'text' => $toc[$prev]['text']); } if ($next != -1) { $next = array('link' => $toc[$next]['link'], 'text' => $toc[$next]['text']); } } /** * You should typically capture this by doing: * list( $toc, $prev, $next, $start ) = $ponydocstoc->loadContent(); * * @FIXME: Previous and next links change based on the page you are on, so we cannot CACHE those! * * $obj = new stdClass(); * $obj->toc = $toc; * $obj->prev = $prev; * $obj->next = $next; * $obj->start = $start; * $cache->addKey($tocKey, $obj); */ // Last but not least, get the manual description if there is one. if (is_object($this->pTOCArticle) && preg_match('/{{#manualDescription:([^}]*)}}/', $this->pTOCArticle->getContent(), $matches)) { $this->mManualDescription = $matches[1]; } // $this->pTOCArticle is empty, we're probably creating a new TOC } else { $toc = array(); $prev = array(); $next = array(); $start = array(); } return array($toc, $prev, $next, $start); }
/** * Create a URL path (e.g. Documentation/Foo/latest/Bar/Bas) for a Topic * * @param string $productName * @param string $manualName * @param string $topicName * @param string $versionName - Optional. We'll get the selected version (which defaults to 'latest') if empty * * @return string * * TODO: We should really be passing a topic object into this and not a string */ public static function getTopicURLPath($productName, $manualName, $topicName, $versionName = NULL) { global $wgArticlePath; if (!isset($versionName)) { $versionName = PonyDocsProductVersion::GetSelectedVersion($productName); } $latestVersion = PonyDocsProductVersion::GetLatestReleasedVersion($productName); if ($latestVersion) { if ($versionName == $latestVersion->getVersionName()) { $versionName = 'latest'; } } $base = str_replace('$1', PONYDOCS_DOCUMENTATION_NAMESPACE_NAME, $wgArticlePath); return "{$base}/{$productName}/{$versionName}/{$manualName}/{$topicName}"; }