Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 109
BookRenderingMediator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
380
0.00% covered (danger)
0.00%
0 / 109
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 9
 getBook
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 20
 getBookFromCache
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 17
 getBookByCacheKey
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 outputBook
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 11
 outputPdf
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 24
 getRestServiceClient
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 26
<?php
namespace MediaWiki\Extension\Collection;
use Config;
use DerivativeContext;
use ErrorPageError;
use Exception;
use MediaWiki\MediaWikiServices;
use OutputPage;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use RequestContext;
use RestbaseVirtualRESTService;
use SkinApi;
use Status;
use TemplateParser;
use VirtualRESTServiceClient;
use WANObjectCache;
/**
 * A mediator class to tie together various aspects of book rendering.
 */
class BookRenderingMediator implements LoggerAwareInterface {
    use LoggerAwareTrait;
    /** @var WANObjectCache */
    private $htmlCache;
    /** @var VirtualRESTServiceClient */
    private $restServiceClient;
    /** @var TemplateParser */
    private $templateParser;
    public function __construct(
        WANObjectCache $bookCache,
        VirtualRESTServiceClient $restServiceClient,
        TemplateParser $templateParser
    ) {
        $this->htmlCache = $bookCache;
        $this->restServiceClient = $restServiceClient;
        $this->templateParser = $templateParser;
        $this->logger = new NullLogger();
    }
    /**
     * Render the HTML source and metadata for the book.
     * @param array[] $collection Collection data, as returned by CollectionSession::getCollection().
     * @return mixed[] associative array with:
     *    - 'html': HTML, not including <body> or anything outside that.
     *    - 'modules', 'modulestyles', 'jsconfigvars': ResourceLoader data
     * @throws ErrorPageError When one of the internal API calls fails.
     */
    public function getBook( array $collection ) {
        $dataProvider = new DataProvider( $this->restServiceClient );
        $dataProvider->setLogger( $this->logger );
        $bookRenderer = new BookRenderer( $this->templateParser );
        $status = $dataProvider->fetchPages( $collection );
        if ( $status->isOK() ) {
            $pages = $status->getValue();
        } else {
            $message = Status::wrap( $status )->getMessage( false, 'coll-rendererror-fetch-wrapper' );
            throw new ErrorPageError( 'coll-rendererror-title', $message );
        }
        $status = $dataProvider->fetchMetadata( array_keys( $pages ) );
        if ( $status->isOK() ) {
            $metadata = $status->getValue();
        } else {
            throw new ErrorPageError( 'coll-rendererror-title', Status::wrap( $status )->getMessage() );
        }
        $html = $bookRenderer->renderBook( $collection, $pages, $metadata );
        $fields = [ 'html', 'modules', 'modulestyles', 'jsconfigvars' ];
        return array_intersect_key( [ 'html' => $html ] + $metadata, array_fill_keys( $fields, null ) );
    }
    /**
     * Get the HTML source and metadata for the book, from cache or by rendering it. Cache it if
     * it wasn't cached before.
     * @param array[] $collection Collection data, as returned by CollectionSession::getCollection().
     * @return mixed[] See getBook(); also there will be a 'key' field with the WAN cache key.
     */
    public function getBookFromCache( array $collection ) {
        // ignore irrelevant parts of the book definition
        $keyBase = array_filter( $collection, static function ( $key ) {
            return in_array( $key, [ 'title', 'subtitle', 'items' ], true );
        }, ARRAY_FILTER_USE_KEY );
        $keyBase['items'] = array_map( static function ( $item ) {
            return array_filter( $item, static function ( $key ) {
                return in_array( $key, [ 'type', 'title', 'revision' ], true );
            }, ARRAY_FILTER_USE_KEY );
        }, $keyBase['items'] );
        $key = $this->htmlCache->makeGlobalKey(
            'collection-book',
            md5( json_encode( $keyBase ) )
        );
        $book = $this->htmlCache->get( $key );
        if ( !$book ) {
            $book = $this->getBook( $collection );
            $book['key'] = $key;
            $this->htmlCache->set( $key, $book, 300 );
        }
        return $book;
    }
    /**
     * @param string $key WAN cache key provided by some past getBook[FromCache] call.
     * @return mixed[]|false Book data (see getBookFromCache()) or false if the cache key wasn't valid.
     */
    public function getBookByCacheKey( $key ) {
        return $this->htmlCache->get( $key );
    }
    /**
     * Append a book to the output.
     * @param array $book See getBookFromCache().
     * @param OutputPage $out Output to send to.
     * @param bool $raw If true, set the output page to raw (no skin).
     */
    public function outputBook( array $book, OutputPage $out, $raw = true ) {
        if ( $raw ) {
            $context = new DerivativeContext( $out->getContext() );
            $context->setSkin( new SkinApi() );
            $out->setContext( $context );
        }
        $out->addModuleStyles( [ 'ext.collection.offline' ] );
        $out->addModules( $book['modules'] );
        $out->addModuleStyles( $book['modulestyles'] );
        $out->addJsConfigVars( $book['jsconfigvars'] );
        $out->addHTML( $book['html'] );
    }
    /**
     * Render a book with Electron and return the response.
     * @param string $publicBookUrl A URL displaying the book. It cannot depend on cookies.
     * @param OutputPage $out Output to send to.
     * @throws ErrorPageError
     */
    public function outputPdf( $publicBookUrl, OutputPage $out ) {
        // Collection have some settings such as pag    e size which could be passed to
        // Electron, but they don't seem to be exposed, and we ignore at least columnt
        // count anyway, so let's ignore the rest as well.
        $electronResponse = $this->restServiceClient->run( [
            'method' => 'GET',
            'url' => '/electron/pdf?' . wfArrayToCgi( [ 'url' => $publicBookUrl ] ),
        ] );
        $errorMsg = null;
        if ( $electronResponse['error'] !== '' ) {
            $message = $out->msg( 'coll-rendererror-pdf', $electronResponse['error'] );
            throw new ErrorPageError( 'coll-rendererror-title', $message );
        } elseif ( $electronResponse['code'] !== 200 ) {
            $errorMsg = $electronResponse['code'];
            if ( $electronResponse['reason'] !== '' ) {
                $errorMsg .= ' ' . $electronResponse['reason'];
            }
            $message = $out->msg( 'coll-rendererror-pdf', $errorMsg );
            throw new ErrorPageError( 'coll-rendererror-title', $message );
        }
        $newPdfContent = $electronResponse['body'];
        $out->disable();
        $mediawikiResponse = $out->getContext()->getRequest()->response();
        $mediawikiResponse->statusHeader( 200 );
        $mediawikiResponse->header( 'Content-Type: application/pdf' );
        $mediawikiResponse->header( "Content-Disposition: attachment; filename='book.pdf'" );
        echo $newPdfContent;
    }
    /**
     * Get a REST client instance.
     * @param Config $config Site configuration
     * @param LoggerInterface $logger
     * @return VirtualRESTServiceClient
     * @throws Exception When $wgVirtualRestConfig is not properly configured.
     */
    public static function getRestServiceClient( Config $config, LoggerInterface $logger ) {
        // This method doesn't really belong to a mediator class; ideally we would just call
        // MediaWikiServices::getVirtualRESTServiceClient(), but that seems to
        // expect a completely different structure for $wgVirtualRestConfig to
        // what is actually there in either WMF production or Vagrant. See T175224.
        $client = new VirtualRESTServiceClient(
            MediaWikiServices::getInstance()->getHttpRequestFactory()->createMultiClient( [
                'logger' => $logger,
            ] )
        );
        $config = $config->get( 'VirtualRestConfig' );
        $modules = [
            'restbase' => RestbaseVirtualRESTService::class,
            'electron' => ElectronVirtualRestService::class,
        ];
        foreach ( $modules as $module => $class ) {
            if ( !isset( $config['modules'][$module] ) || !is_array( $config['modules'][$module] ) ) {
                throw new Exception( "VirtualRESTService module $module is not configured propely" );
            }
            $params = $config['modules'][$module];
            if ( isset( $config['global'] ) ) {
                $params += $config['global'];
            }
            if ( $params['forwardCookies'] ) {
                $params['forwardCookies'] =
                    RequestContext::getMain()->getRequest()->getHeader( 'Cookie' );
            }
            $client->mount( "/$module/", [ 'class' => $class, 'config' => $params ] );
        }
        return $client;
    }
}