category = $cat; $this->maxTimestamp = null; $this->minTimestamp = null; } protected function getFormFields() { $this->addHelpLink( 'Help:RandomInCategory' ); return array( 'category' => array( 'type' => 'title', 'namespace' => NS_CATEGORY, 'relative' => true, 'label-message' => 'randomincategory-category', 'required' => true, ) ); } public function requiresWrite() { return false; } public function requiresUnblock() { return false; } protected function getDisplayFormat() { return 'ooui'; } protected function alterForm( HTMLForm $form ) { $form->setSubmitTextMsg( 'randomincategory-submit' ); } protected function setParameter( $par ) { // if subpage present, fake form submission $this->onSubmit( array( 'category' => $par ) ); } public function onSubmit( array $data ) { $cat = false; $categoryStr = $data['category']; if ( $categoryStr ) { $cat = Title::newFromText( $categoryStr, NS_CATEGORY ); } if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) { // Someone searching for something like "Wikipedia:Foo" $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr ); } if ( $cat ) { $this->setCategory( $cat ); } if ( !$this->category && $categoryStr ) { $msg = $this->msg( 'randomincategory-invalidcategory', wfEscapeWikiText( $categoryStr ) ); return Status::newFatal( $msg ); } elseif ( !$this->category ) { return false; // no data sent } $title = $this->getRandomTitle(); if ( is_null( $title ) ) { $msg = $this->msg( 'randomincategory-nopages', $this->category->getText() ); return Status::newFatal( $msg ); } $this->getOutput()->redirect( $title->getFullURL() ); } /** * Choose a random title. * @return Title|null Title object (or null if nothing to choose from) */ public function getRandomTitle() { // Convert to float, since we do math with the random number. $rand = (float)wfRandom(); $title = null; // Given that timestamps are rather unevenly distributed, we also // use an offset between 0 and 30 to make any biases less noticeable. $offset = mt_rand( 0, $this->maxOffset ); if ( mt_rand( 0, 1 ) ) { $up = true; } else { $up = false; } $row = $this->selectRandomPageFromDB( $rand, $offset, $up ); // Try again without the timestamp offset (wrap around the end) if ( !$row ) { $row = $this->selectRandomPageFromDB( false, $offset, $up ); } // Maybe the category is really small and offset too high if ( !$row ) { $row = $this->selectRandomPageFromDB( $rand, 0, $up ); } // Just get the first entry. if ( !$row ) { $row = $this->selectRandomPageFromDB( false, 0, true ); } if ( $row ) { return Title::makeTitle( $row->page_namespace, $row->page_title ); } return null; } /** * @param float $rand Random number between 0 and 1 * @param int $offset Extra offset to fudge randomness * @param bool $up True to get the result above the random number, false for below * @return array Query information. * @throws MWException * @note The $up parameter is supposed to counteract what would happen if there * was a large gap in the distribution of cl_timestamp values. This way instead * of things to the right of the gap being favoured, both sides of the gap * are favoured. */ protected function getQueryInfo( $rand, $offset, $up ) { $op = $up ? '>=' : '<='; $dir = $up ? 'ASC' : 'DESC'; if ( !$this->category instanceof Title ) { throw new MWException( 'No category set' ); } $qi = array( 'tables' => array( 'categorylinks', 'page' ), 'fields' => array( 'page_title', 'page_namespace' ), 'conds' => array_merge( array( 'cl_to' => $this->category->getDBKey(), ), $this->extra ), 'options' => array( 'ORDER BY' => 'cl_timestamp ' . $dir, 'LIMIT' => 1, 'OFFSET' => $offset ), 'join_conds' => array( 'page' => array( 'INNER JOIN', 'cl_from = page_id' ) ) ); $dbr = wfGetDB( DB_SLAVE ); $minClTime = $this->getTimestampOffset( $rand ); if ( $minClTime ) { $qi['conds'][] = 'cl_timestamp ' . $op . ' ' . $dbr->addQuotes( $dbr->timestamp( $minClTime ) ); } return $qi; } /** * @param float $rand Random number between 0 and 1 * * @return int|bool A random (unix) timestamp from the range of the category or false on failure */ protected function getTimestampOffset( $rand ) { if ( $rand === false ) { return false; } if ( !$this->minTimestamp || !$this->maxTimestamp ) { try { list( $this->minTimestamp, $this->maxTimestamp ) = $this->getMinAndMaxForCat( $this->category ); } catch ( Exception $e ) { // Possibly no entries in category. return false; } } $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp; return intval( $ts ); } /** * Get the lowest and highest timestamp for a category. * * @param Title $category * @return array The lowest and highest timestamp * @throws MWException If category has no entries. */ protected function getMinAndMaxForCat( Title $category ) { $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->selectRow( 'categorylinks', array( 'low' => 'MIN( cl_timestamp )', 'high' => 'MAX( cl_timestamp )' ), array( 'cl_to' => $this->category->getDBKey(), ), __METHOD__, array( 'LIMIT' => 1 ) ); if ( !$res ) { throw new MWException( 'No entries in category' ); } return array( wfTimestamp( TS_UNIX, $res->low ), wfTimestamp( TS_UNIX, $res->high ) ); } /** * @param float $rand A random number that is converted to a random timestamp * @param int $offset A small offset to make the result seem more "random" * @param bool $up Get the result above the random value * @param string $fname The name of the calling method * @return array Info for the title selected. */ private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) { $dbr = wfGetDB( DB_SLAVE ); $query = $this->getQueryInfo( $rand, $offset, $up ); $res = $dbr->select( $query['tables'], $query['fields'], $query['conds'], $fname, $query['options'], $query['join_conds'] ); return $res->fetchObject(); } protected function getGroupName() { return 'redirects'; } }