configFactory = $configFactory; $this->languageConverterFactory = $languageConverterFactory; $this->linkRenderer = $linkRenderer; $this->dbProvider = $dbProvider; $this->wanCache = $wanCache; } /** * @inheritDoc */ public function execute() { $params = $this->extractRequestParams(); $options = $this->extractOptions( $params ); $title = CategoryTree::makeTitle( $params['category'] ); if ( !$title || $title->isExternal() ) { $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['category'] ) ] ); } $depth = isset( $options['depth'] ) ? (int)$options['depth'] : 1; $ct = new CategoryTree( $options, $this->getConfig(), $this->dbProvider, $this->linkRenderer ); $depth = $ct->optionManager->capDepth( $depth ); $ctConfig = $this->configFactory->makeConfig( 'categorytree' ); $html = $this->getHTML( $ct, $title, $depth, $ctConfig ); $this->getMain()->setCacheMode( 'public' ); $this->getResult()->addContentValue( $this->getModuleName(), 'html', $html ); } /** * @param array $params * @return string[] */ private function extractOptions( array $params ): array { if ( !isset( $params['options'] ) ) { return []; } $options = FormatJson::decode( $params['options'] ); if ( !is_object( $options ) ) { $this->dieWithError( 'apierror-categorytree-invalidjson', 'invalidjson' ); } foreach ( $options as $option => $value ) { if ( is_scalar( $value ) || $value === null ) { continue; } if ( $option === 'namespaces' && is_array( $value ) ) { continue; } $this->dieWithError( [ 'apierror-categorytree-invalidjson-option', $option ], 'invalidjson-option' ); } return get_object_vars( $options ); } /** * @param string $condition * * @return bool|null|string */ public function getConditionalRequestData( $condition ) { if ( $condition === 'last-modified' ) { $params = $this->extractRequestParams(); $title = CategoryTree::makeTitle( $params['category'] ); return $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() ->select( 'page_touched' ) ->from( 'page' ) ->where( [ 'page_namespace' => NS_CATEGORY, 'page_title' => $title->getDBkey(), ] ) ->caller( __METHOD__ ) ->fetchField(); } } /** * Get category tree HTML for the given tree, title, depth and config * * @param CategoryTree $ct * @param Title $title * @param int $depth * @param Config $ctConfig Config for CategoryTree * @return string HTML */ private function getHTML( CategoryTree $ct, Title $title, int $depth, Config $ctConfig ): string { $langConv = $this->languageConverterFactory->getLanguageConverter(); return $this->wanCache->getWithSetCallback( $this->wanCache->makeKey( 'categorytree-html-ajax', md5( $title->getDBkey() ), md5( $ct->optionManager->getOptionsAsCacheKey( $depth ) ), $this->getLanguage()->getCode(), $langConv->getExtraHashOptions(), $ctConfig->get( 'RenderHashAppend' ) ), $this->wanCache::TTL_DAY, static function () use ( $ct, $title, $depth ) { return trim( $ct->renderChildren( $title, $depth ) ); }, [ 'touchedCallback' => function () { $timestamp = $this->getConditionalRequestData( 'last-modified' ); return $timestamp ? wfTimestamp( TS_UNIX, $timestamp ) : null; } ] ); } /** * @inheritDoc */ public function getAllowedParams() { return [ 'category' => [ ParamValidator::PARAM_TYPE => 'string', ParamValidator::PARAM_REQUIRED => true, ], 'options' => [ ParamValidator::PARAM_TYPE => 'string', ], ]; } /** * @inheritDoc */ public function isInternal() { return true; } }