diff --git a/includes/ApiCategoryTree.php b/includes/ApiCategoryTree.php index 3c4042c1..197e1bc3 100644 --- a/includes/ApiCategoryTree.php +++ b/includes/ApiCategoryTree.php @@ -82,7 +82,7 @@ class ApiCategoryTree extends ApiBase { $depth = isset( $options['depth'] ) ? (int)$options['depth'] : 1; $ct = new CategoryTree( $options ); - $depth = CategoryTree::capDepth( $ct->getOption( 'mode' ), $depth ); + $depth = OptionManager::capDepth( $ct->optionManager->getOption( 'mode' ), $depth ); $ctConfig = $this->configFactory->makeConfig( 'categorytree' ); $html = $this->getHTML( $ct, $title, $depth, $ctConfig ); @@ -155,7 +155,7 @@ class ApiCategoryTree extends ApiBase { $this->wanCache->makeKey( 'categorytree-html-ajax', md5( $title->getDBkey() ), - md5( $ct->getOptionsAsCacheKey( $depth ) ), + md5( $ct->optionManager->getOptionsAsCacheKey( $depth ) ), $this->getLanguage()->getCode(), $langConv->getExtraHashOptions(), $ctConfig->get( 'RenderHashAppend' ) diff --git a/includes/CategoryTree.php b/includes/CategoryTree.php index b05aa038..6abc9ac2 100644 --- a/includes/CategoryTree.php +++ b/includes/CategoryTree.php @@ -24,9 +24,7 @@ namespace MediaWiki\Extension\CategoryTree; -use Exception; use ExtensionRegistry; -use FormatJson; use IContextSource; use MediaWiki\Category\Category; use MediaWiki\Html\Html; @@ -43,8 +41,8 @@ use RequestContext; * to display the category structure of a wiki */ class CategoryTree { - /** @var array */ - private $mOptions = []; + /** @var OptionManager */ + public $optionManager; /** @var LinkRenderer */ private $linkRenderer; @@ -53,212 +51,8 @@ class CategoryTree { * @param array $options */ public function __construct( array $options ) { - global $wgCategoryTreeDefaultOptions; + $this->optionManager = new OptionManager( $options ); $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); - - // ensure default values and order of options. - // Order may become important, it may influence the cache key! - foreach ( $wgCategoryTreeDefaultOptions as $option => $default ) { - $this->mOptions[$option] = $options[$option] ?? $default; - } - - $this->mOptions['mode'] = self::decodeMode( $this->mOptions['mode'] ); - - if ( $this->mOptions['mode'] === CategoryTreeMode::PARENTS ) { - // namespace filter makes no sense with CategoryTreeMode::PARENTS - $this->mOptions['namespaces'] = false; - } - - $this->mOptions['hideprefix'] = self::decodeHidePrefix( $this->mOptions['hideprefix'] ); - $this->mOptions['showcount'] = self::decodeBoolean( $this->mOptions['showcount'] ); - $this->mOptions['namespaces'] = self::decodeNamespaces( $this->mOptions['namespaces'] ); - - if ( $this->mOptions['namespaces'] ) { - # automatically adjust mode to match namespace filter - if ( count( $this->mOptions['namespaces'] ) === 1 - && $this->mOptions['namespaces'][0] === NS_CATEGORY ) { - $this->mOptions['mode'] = CategoryTreeMode::CATEGORIES; - } elseif ( !in_array( NS_FILE, $this->mOptions['namespaces'] ) ) { - $this->mOptions['mode'] = CategoryTreeMode::PAGES; - } else { - $this->mOptions['mode'] = CategoryTreeMode::ALL; - } - } - } - - /** - * @param string $name - * @return mixed - */ - public function getOption( $name ) { - return $this->mOptions[$name]; - } - - /** - * @return bool - */ - private function isInverse() { - return $this->getOption( 'mode' ) === CategoryTreeMode::PARENTS; - } - - /** - * @param mixed $nn - * @return array|bool - */ - private static function decodeNamespaces( $nn ) { - if ( $nn === false || $nn === null ) { - return false; - } - - if ( !is_array( $nn ) ) { - $nn = preg_split( '![\s#:|]+!', $nn ); - } - - $namespaces = []; - $contLang = MediaWikiServices::getInstance()->getContentLanguage(); - foreach ( $nn as $n ) { - if ( is_int( $n ) ) { - $ns = $n; - } else { - $n = trim( $n ); - if ( $n === '' ) { - continue; - } - - $lower = strtolower( $n ); - - if ( is_numeric( $n ) ) { - $ns = (int)$n; - } elseif ( $n === '-' || $n === '_' || $n === '*' || $lower === 'main' ) { - $ns = NS_MAIN; - } else { - $ns = $contLang->getNsIndex( $n ); - } - } - - if ( is_int( $ns ) ) { - $namespaces[] = $ns; - } - } - - # get elements into canonical order - sort( $namespaces ); - return $namespaces; - } - - /** - * @param mixed $mode - * @return int|string - */ - public static function decodeMode( $mode ) { - global $wgCategoryTreeDefaultOptions; - - if ( $mode === null ) { - return $wgCategoryTreeDefaultOptions['mode']; - } - if ( is_int( $mode ) ) { - return $mode; - } - - $mode = trim( strtolower( $mode ) ); - - if ( is_numeric( $mode ) ) { - return (int)$mode; - } - - if ( $mode === 'all' ) { - $mode = CategoryTreeMode::ALL; - } elseif ( $mode === 'pages' ) { - $mode = CategoryTreeMode::PAGES; - } elseif ( $mode === 'categories' || $mode === 'sub' ) { - $mode = CategoryTreeMode::CATEGORIES; - } elseif ( $mode === 'parents' || $mode === 'super' || $mode === 'inverse' ) { - $mode = CategoryTreeMode::PARENTS; - } elseif ( $mode === 'default' ) { - $mode = $wgCategoryTreeDefaultOptions['mode']; - } - - return (int)$mode; - } - - /** - * Helper function to convert a string to a boolean value. - * Perhaps make this a global function in MediaWiki proper - * @param mixed $value - * @return bool|null|string - */ - public static function decodeBoolean( $value ) { - if ( $value === null ) { - return null; - } - if ( is_bool( $value ) ) { - return $value; - } - if ( is_int( $value ) ) { - return ( $value > 0 ); - } - - $value = trim( strtolower( $value ) ); - if ( is_numeric( $value ) ) { - return ( (int)$value > 0 ); - } - - if ( $value === 'yes' || $value === 'y' - || $value === 'true' || $value === 't' || $value === 'on' - ) { - return true; - } elseif ( $value === 'no' || $value === 'n' - || $value === 'false' || $value === 'f' || $value === 'off' - ) { - return false; - } elseif ( $value === 'null' || $value === 'default' || $value === 'none' || $value === 'x' ) { - return null; - } else { - return false; - } - } - - /** - * @param mixed $value - * @return int|string - */ - public static function decodeHidePrefix( $value ) { - global $wgCategoryTreeDefaultOptions; - - if ( $value === null ) { - return $wgCategoryTreeDefaultOptions['hideprefix']; - } - if ( is_int( $value ) ) { - return $value; - } - if ( $value === true ) { - return CategoryTreeHidePrefix::ALWAYS; - } - if ( $value === false ) { - return CategoryTreeHidePrefix::NEVER; - } - - $value = trim( strtolower( $value ) ); - - if ( $value === 'yes' || $value === 'y' - || $value === 'true' || $value === 't' || $value === 'on' - ) { - return CategoryTreeHidePrefix::ALWAYS; - } elseif ( $value === 'no' || $value === 'n' - || $value === 'false' || $value === 'f' || $value === 'off' - ) { - return CategoryTreeHidePrefix::NEVER; - } elseif ( $value === 'always' ) { - return CategoryTreeHidePrefix::ALWAYS; - } elseif ( $value === 'never' ) { - return CategoryTreeHidePrefix::NEVER; - } elseif ( $value === 'auto' ) { - return CategoryTreeHidePrefix::AUTO; - } elseif ( $value === 'categories' || $value === 'category' || $value === 'smart' ) { - return CategoryTreeHidePrefix::CATEGORIES; - } else { - return $wgCategoryTreeDefaultOptions['hideprefix']; - } } /** @@ -271,57 +65,6 @@ class CategoryTree { $outputPage->addModules( 'ext.categoryTree' ); } - /** - * @param array $options - * @param string $enc - * @return mixed - * @throws Exception - */ - protected static function encodeOptions( array $options, $enc ) { - if ( $enc === 'mode' || $enc === '' ) { - $opt = $options['mode']; - } elseif ( $enc === 'json' ) { - $opt = FormatJson::encode( $options ); - } else { - throw new Exception( 'Unknown encoding for CategoryTree options: ' . $enc ); - } - - return $opt; - } - - /** - * @param int|null $depth - * @return string - */ - public function getOptionsAsCacheKey( $depth = null ) { - $key = ''; - - foreach ( $this->mOptions as $k => $v ) { - if ( is_array( $v ) ) { - $v = implode( '|', $v ); - } - $key .= $k . ':' . $v . ';'; - } - - if ( $depth !== null ) { - $key .= ';depth=' . $depth; - } - return $key; - } - - /** - * @param int|null $depth - * @return mixed - */ - public function getOptionsAsJsStructure( $depth = null ) { - $opt = $this->mOptions; - if ( $depth !== null ) { - $opt['depth'] = $depth; - } - - return self::encodeOptions( $opt, 'json' ); - } - /** * Custom tag implementation. This is called by Hooks::parserHook, which is used to * load CategoryTreeFunctions.php on demand. @@ -363,8 +106,8 @@ class CategoryTree { $attr['class'] = 'CategoryTreeTag'; } - $attr['data-ct-mode'] = $this->mOptions['mode']; - $attr['data-ct-options'] = $this->getOptionsAsJsStructure(); + $attr['data-ct-mode'] = $this->optionManager->getOption( 'mode' ); + $attr['data-ct-options'] = $this->optionManager->getOptionsAsJsStructure(); if ( !$allowMissing && !$title->getArticleID() ) { $html = Html::rawElement( 'span', [ 'class' => 'CategoryTreeNotice' ], @@ -400,9 +143,9 @@ class CategoryTree { $dbr = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getReplicaDatabase(); - $inverse = $this->isInverse(); - $mode = $this->getOption( 'mode' ); - $namespaces = $this->getOption( 'namespaces' ); + $inverse = $this->optionManager->isInverse(); + $mode = $this->optionManager->getOption( 'mode' ); + $namespaces = $this->optionManager->getOption( 'namespaces' ); $tables = [ 'page', 'categorylinks' ]; $fields = [ 'page_id', 'page_namespace', 'page_title', @@ -454,8 +197,8 @@ class CategoryTree { # collect categories separately from other pages $categories = ''; $other = ''; - $suppressTranslations = self::decodeBoolean( - $this->getOption( 'notranslations' ) + $suppressTranslations = OptionManager::decodeBoolean( + $this->optionManager->getOption( 'notranslations' ) ) && ExtensionRegistry::getInstance()->isLoaded( 'Translate' ); if ( $suppressTranslations ) { @@ -544,7 +287,7 @@ class CategoryTree { $special, $t->getText(), [ 'class' => 'CategoryTreeLabel' ], - [ 'target' => $t->getDBkey() ] + $this->mOptions + [ 'target' => $t->getDBkey() ] + $this->optionManager->getOptions() ) ); } @@ -562,7 +305,7 @@ class CategoryTree { global $wgCategoryTreeUseCategoryTable; if ( $wgCategoryTreeUseCategoryTable && $title->inNamespace( NS_CATEGORY ) - && !$this->isInverse() + && !$this->optionManager->isInverse() ) { $cat = Category::newFromTitle( $title ); } else { @@ -581,12 +324,12 @@ class CategoryTree { * @return string */ public function renderNodeInfo( Title $title, Category $cat = null, $children = 0 ) { - $mode = $this->getOption( 'mode' ); + $mode = $this->optionManager->getOption( 'mode' ); $isInCatNS = $title->inNamespace( NS_CATEGORY ); $key = $title->getDBkey(); - $hideprefix = $this->getOption( 'hideprefix' ); + $hideprefix = $this->optionManager->getOption( 'hideprefix' ); if ( $hideprefix === CategoryTreeHidePrefix::ALWAYS ) { $hideprefix = true; @@ -657,7 +400,7 @@ class CategoryTree { $s .= $link; - if ( $count !== false && $this->getOption( 'showcount' ) ) { + if ( $count !== false && $this->optionManager->getOption( 'showcount' ) ) { $s .= self::createCountString( RequestContext::getMain(), $cat, $count ); } @@ -777,31 +520,4 @@ class CategoryTree { } return $t; } - - /** - * Internal function to cap depth - * @param string $mode - * @param int $depth - * @return int|mixed - */ - public static function capDepth( $mode, $depth ) { - global $wgCategoryTreeMaxDepth; - - if ( !is_numeric( $depth ) ) { - return 1; - } - - $depth = intval( $depth ); - - if ( is_array( $wgCategoryTreeMaxDepth ) ) { - $max = $wgCategoryTreeMaxDepth[$mode] ?? 1; - } elseif ( is_numeric( $wgCategoryTreeMaxDepth ) ) { - $max = $wgCategoryTreeMaxDepth; - } else { - wfDebug( 'CategoryTree::capDepth: $wgCategoryTreeMaxDepth is invalid.' ); - $max = 1; - } - - return min( $depth, $max ); - } } diff --git a/includes/CategoryTreePage.php b/includes/CategoryTreePage.php index d9792d32..a3f7e698 100644 --- a/includes/CategoryTreePage.php +++ b/includes/CategoryTreePage.php @@ -60,7 +60,7 @@ class CategoryTreePage extends SpecialPage { */ private function getOption( $name ) { if ( $this->tree ) { - return $this->tree->getOption( $name ); + return $this->tree->optionManager->getOption( $name ); } else { return $this->getConfig()->get( 'CategoryTreeDefaultOptions' )[$name]; } @@ -197,8 +197,8 @@ class CategoryTreePage extends SpecialPage { $output->addHTML( Html::rawElement( 'div', [ 'class' => 'CategoryTreeResult CategoryTreeTag', - 'data-ct-mode' => $this->tree->getOption( 'mode' ), - 'data-ct-options' => $this->tree->getOptionsAsJsStructure(), + 'data-ct-mode' => $this->tree->optionManager->getOption( 'mode' ), + 'data-ct-options' => $this->tree->optionManager->getOptionsAsJsStructure(), ], $this->tree->renderNode( $title, 1 ) ) ); diff --git a/includes/Hooks.php b/includes/Hooks.php index c9b2ad11..d40621dd 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -198,12 +198,12 @@ class Hooks implements $attr = Sanitizer::validateTagAttributes( $argv, 'div' ); $hideroot = isset( $argv['hideroot'] ) - ? CategoryTree::decodeBoolean( $argv['hideroot'] ) : null; + ? OptionManager::decodeBoolean( $argv['hideroot'] ) : null; $onlyroot = isset( $argv['onlyroot'] ) - ? CategoryTree::decodeBoolean( $argv['onlyroot'] ) : null; + ? OptionManager::decodeBoolean( $argv['onlyroot'] ) : null; $depthArg = isset( $argv['depth'] ) ? (int)$argv['depth'] : null; - $depth = CategoryTree::capDepth( $ct->getOption( 'mode' ), $depthArg ); + $depth = OptionManager::capDepth( $ct->optionManager->getOption( 'mode' ), $depthArg ); if ( $onlyroot ) { $depth = 0; } @@ -243,10 +243,10 @@ class Hooks implements */ public static function getDataForJs( RL\Context $context, Config $config ) { // Look, this is pretty bad but CategoryTree is just whacky, it needs to be rewritten - $ct = new CategoryTree( $config->get( 'CategoryTreeCategoryPageOptions' ) ); + $optionManager = new OptionManager( $config->get( 'CategoryTreeCategoryPageOptions' ) ); return [ - 'defaultCtOptions' => $ct->getOptionsAsJsStructure(), + 'defaultCtOptions' => $optionManager->getOptionsAsJsStructure(), ]; } diff --git a/includes/OptionManager.php b/includes/OptionManager.php new file mode 100644 index 00000000..a177ab0e --- /dev/null +++ b/includes/OptionManager.php @@ -0,0 +1,333 @@ + $default ) { + $this->mOptions[$option] = $options[$option] ?? $default; + } + + $this->mOptions['mode'] = self::decodeMode( $this->mOptions['mode'] ); + + if ( $this->mOptions['mode'] === CategoryTreeMode::PARENTS ) { + // namespace filter makes no sense with CategoryTreeMode::PARENTS + $this->mOptions['namespaces'] = false; + } + + $this->mOptions['hideprefix'] = self::decodeHidePrefix( $this->mOptions['hideprefix'] ); + $this->mOptions['showcount'] = self::decodeBoolean( $this->mOptions['showcount'] ); + $this->mOptions['namespaces'] = self::decodeNamespaces( $this->mOptions['namespaces'] ); + + if ( $this->mOptions['namespaces'] ) { + # automatically adjust mode to match namespace filter + if ( count( $this->mOptions['namespaces'] ) === 1 + && $this->mOptions['namespaces'][0] === NS_CATEGORY ) { + $this->mOptions['mode'] = CategoryTreeMode::CATEGORIES; + } elseif ( !in_array( NS_FILE, $this->mOptions['namespaces'] ) ) { + $this->mOptions['mode'] = CategoryTreeMode::PAGES; + } else { + $this->mOptions['mode'] = CategoryTreeMode::ALL; + } + } + } + + /** + * @return array + */ + public function getOptions() { + return $this->mOptions; + } + + /** + * @param string $name + * @return mixed + */ + public function getOption( $name ) { + return $this->mOptions[$name]; + } + + /** + * @return bool + */ + public function isInverse() { + return $this->getOption( 'mode' ) === CategoryTreeMode::PARENTS; + } + + /** + * @param mixed $nn + * @return array|bool + */ + private static function decodeNamespaces( $nn ) { + if ( $nn === false || $nn === null ) { + return false; + } + + if ( !is_array( $nn ) ) { + $nn = preg_split( '![\s#:|]+!', $nn ); + } + + $namespaces = []; + $contLang = MediaWikiServices::getInstance()->getContentLanguage(); + foreach ( $nn as $n ) { + if ( is_int( $n ) ) { + $ns = $n; + } else { + $n = trim( $n ); + if ( $n === '' ) { + continue; + } + + $lower = strtolower( $n ); + + if ( is_numeric( $n ) ) { + $ns = (int)$n; + } elseif ( $n === '-' || $n === '_' || $n === '*' || $lower === 'main' ) { + $ns = NS_MAIN; + } else { + $ns = $contLang->getNsIndex( $n ); + } + } + + if ( is_int( $ns ) ) { + $namespaces[] = $ns; + } + } + + # get elements into canonical order + sort( $namespaces ); + return $namespaces; + } + + /** + * @param mixed $mode + * @return int|string + */ + private static function decodeMode( $mode ) { + global $wgCategoryTreeDefaultOptions; + + if ( $mode === null ) { + return $wgCategoryTreeDefaultOptions['mode']; + } + if ( is_int( $mode ) ) { + return $mode; + } + + $mode = trim( strtolower( $mode ) ); + + if ( is_numeric( $mode ) ) { + return (int)$mode; + } + + if ( $mode === 'all' ) { + $mode = CategoryTreeMode::ALL; + } elseif ( $mode === 'pages' ) { + $mode = CategoryTreeMode::PAGES; + } elseif ( $mode === 'categories' || $mode === 'sub' ) { + $mode = CategoryTreeMode::CATEGORIES; + } elseif ( $mode === 'parents' || $mode === 'super' || $mode === 'inverse' ) { + $mode = CategoryTreeMode::PARENTS; + } elseif ( $mode === 'default' ) { + $mode = $wgCategoryTreeDefaultOptions['mode']; + } + + return (int)$mode; + } + + /** + * Helper function to convert a string to a boolean value. + * Perhaps make this a global function in MediaWiki proper + * @param mixed $value + * @return bool|null|string + */ + public static function decodeBoolean( $value ) { + if ( $value === null ) { + return null; + } + if ( is_bool( $value ) ) { + return $value; + } + if ( is_int( $value ) ) { + return ( $value > 0 ); + } + + $value = trim( strtolower( $value ) ); + if ( is_numeric( $value ) ) { + return ( (int)$value > 0 ); + } + + if ( $value === 'yes' || $value === 'y' + || $value === 'true' || $value === 't' || $value === 'on' + ) { + return true; + } elseif ( $value === 'no' || $value === 'n' + || $value === 'false' || $value === 'f' || $value === 'off' + ) { + return false; + } elseif ( $value === 'null' || $value === 'default' || $value === 'none' || $value === 'x' ) { + return null; + } else { + return false; + } + } + + /** + * @param mixed $value + * @return int|string + */ + private static function decodeHidePrefix( $value ) { + global $wgCategoryTreeDefaultOptions; + + if ( $value === null ) { + return $wgCategoryTreeDefaultOptions['hideprefix']; + } + if ( is_int( $value ) ) { + return $value; + } + if ( $value === true ) { + return CategoryTreeHidePrefix::ALWAYS; + } + if ( $value === false ) { + return CategoryTreeHidePrefix::NEVER; + } + + $value = trim( strtolower( $value ) ); + + if ( $value === 'yes' || $value === 'y' + || $value === 'true' || $value === 't' || $value === 'on' + ) { + return CategoryTreeHidePrefix::ALWAYS; + } elseif ( $value === 'no' || $value === 'n' + || $value === 'false' || $value === 'f' || $value === 'off' + ) { + return CategoryTreeHidePrefix::NEVER; + } elseif ( $value === 'always' ) { + return CategoryTreeHidePrefix::ALWAYS; + } elseif ( $value === 'never' ) { + return CategoryTreeHidePrefix::NEVER; + } elseif ( $value === 'auto' ) { + return CategoryTreeHidePrefix::AUTO; + } elseif ( $value === 'categories' || $value === 'category' || $value === 'smart' ) { + return CategoryTreeHidePrefix::CATEGORIES; + } else { + return $wgCategoryTreeDefaultOptions['hideprefix']; + } + } + + /** + * @param array $options + * @param string $enc + * @return mixed + * @throws Exception + */ + private static function encodeOptions( array $options, $enc ) { + if ( $enc === 'mode' || $enc === '' ) { + $opt = $options['mode']; + } elseif ( $enc === 'json' ) { + $opt = FormatJson::encode( $options ); + } else { + throw new Exception( 'Unknown encoding for CategoryTree options: ' . $enc ); + } + + return $opt; + } + + /** + * @param int|null $depth + * @return string + */ + public function getOptionsAsCacheKey( $depth = null ) { + $key = ''; + + foreach ( $this->mOptions as $k => $v ) { + if ( is_array( $v ) ) { + $v = implode( '|', $v ); + } + $key .= $k . ':' . $v . ';'; + } + + if ( $depth !== null ) { + $key .= ';depth=' . $depth; + } + return $key; + } + + /** + * @param int|null $depth + * @return mixed + */ + public function getOptionsAsJsStructure( $depth = null ) { + $opt = $this->mOptions; + if ( $depth !== null ) { + $opt['depth'] = $depth; + } + + return self::encodeOptions( $opt, 'json' ); + } + + /** + * Internal function to cap depth + * @param string $mode + * @param int $depth + * @return int|mixed + */ + public static function capDepth( $mode, $depth ) { + global $wgCategoryTreeMaxDepth; + + if ( !is_numeric( $depth ) ) { + return 1; + } + + $depth = intval( $depth ); + + if ( is_array( $wgCategoryTreeMaxDepth ) ) { + $max = $wgCategoryTreeMaxDepth[$mode] ?? 1; + } elseif ( is_numeric( $wgCategoryTreeMaxDepth ) ) { + $max = $wgCategoryTreeMaxDepth; + } else { + wfDebug( __METHOD__ . ': $wgCategoryTreeMaxDepth is invalid.' ); + $max = 1; + } + + return min( $depth, $max ); + } +}