Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
48.21% covered (danger)
48.21%
27 / 56
58.33% covered (warning)
58.33%
7 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConversionStrategy
48.21% covered (danger)
48.21%
27 / 56
58.33% covered (warning)
58.33%
7 / 12
96.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 getSourceStore
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMoveComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCleanupComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isConversionFinished
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 createImportSource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 decideArchiveTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPostprocessor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createArchiveCleanupRevisionContent
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 meetsSubpageRequirements
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 hasNoConvertTemplate
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 shouldConvert
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace Flow\Import\Wikitext;
4
5use DateTime;
6use DateTimeZone;
7use Flow\Import\ArchiveNameHelper;
8use Flow\Import\IConversionStrategy;
9use Flow\Import\SourceStore\SourceStoreInterface;
10use MediaWiki\Content\WikitextContent;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Parser\Parser;
13use MediaWiki\Registration\ExtensionRegistry;
14use MediaWiki\StubObject\StubObject;
15use MediaWiki\Title\Title;
16use MediaWiki\User\User;
17use Psr\Log\LoggerInterface;
18
19/**
20 * Does not really convert. Archives wikitext pages out of the way and puts
21 * a new flow board in place. We take either the entire page, or the page up
22 * to the first section and put it into the header of the flow board. We
23 * additionally edit both the flow header and the archived page to include
24 * a localized template containing the reciprocal title and the conversion
25 * date in GMT.
26 *
27 * It is plausible something with the EchoDiscussionParser could be worked up
28 * to do an import of topics and posts. We know it wont work for everything,
29 * but we don't know if it works for 90%, 99%, or 99.99% of topics. We know
30 * for sure that it does not currently understand anything about editing an
31 * existing comment.
32 */
33class ConversionStrategy implements IConversionStrategy {
34    /**
35     * @var LoggerInterface
36     */
37    protected $logger;
38
39    /**
40     * @var SourceStoreInterface
41     */
42    protected $sourceStore;
43
44    /**
45     * @var Parser|StubObject
46     */
47    protected $parser;
48
49    /**
50     * @var array
51     */
52    protected $archiveTitleSuggestions;
53
54    /**
55     * @var string
56     */
57    protected $headerSuffix;
58
59    /** @var User User doing the conversion actions (e.g. initial description, wikitext
60     *    archive edit).  However, actions will be attributed to the original user when
61     *    possible (e.g. the user who did the original LQT reply)
62     *
63     */
64    protected $user;
65
66    /**
67     * @var Title[]
68     */
69    private $noConvertTemplates;
70
71    /**
72     * @param Parser|StubObject $parser
73     * @param SourceStoreInterface $sourceStore
74     * @param LoggerInterface $logger
75     * @param User $user User to take conversion actions are (applicable for actions
76     *   where if there is no 'original' user)
77     * @param Title[] $noConvertTemplates List of templates that flag pages that
78     *  shouldn't be converted (optional)
79     * @param string|null $headerSuffix Wikitext to add to the end of the header (optional)
80     */
81    public function __construct(
82        $parser,
83        SourceStoreInterface $sourceStore,
84        LoggerInterface $logger,
85        User $user,
86        array $noConvertTemplates = [],
87        $headerSuffix = null
88    ) {
89        $this->parser = $parser;
90        $this->sourceStore = $sourceStore;
91        $this->logger = $logger;
92        $this->user = $user;
93        $this->noConvertTemplates = $noConvertTemplates;
94        $this->headerSuffix = $headerSuffix;
95
96        $archiveFormat = wfMessage( 'flow-conversion-archive-page-name-format' )->inContentLanguage()->plain();
97        if ( strpos( $archiveFormat, "\n" ) === false ) {
98            $this->archiveTitleSuggestions = [ $archiveFormat ];
99        } else {
100            $this->archiveTitleSuggestions = explode( "\n", $archiveFormat );
101        }
102    }
103
104    /**
105     * @inheritDoc
106     */
107    public function getSourceStore() {
108        return $this->sourceStore;
109    }
110
111    /**
112     * @inheritDoc
113     */
114    public function getMoveComment( Title $from, Title $to ) {
115        return wfMessage( 'flow-talk-conversion-move-reason', $from->getPrefixedText() )->plain();
116    }
117
118    /**
119     * @inheritDoc
120     */
121    public function getCleanupComment( Title $from, Title $to ) {
122        return wfMessage( 'flow-talk-conversion-archive-edit-reason' )->plain();
123    }
124
125    /**
126     * @inheritDoc
127     */
128    public function isConversionFinished( Title $title, ?Title $movedFrom = null ) {
129        if ( $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD ) {
130            // page is a flow board already
131            return true;
132        } elseif ( $movedFrom ) {
133            // page was moved out of the way by import - leave it alone
134            return true;
135        } else {
136            return false;
137        }
138    }
139
140    /**
141     * @inheritDoc
142     */
143    public function createImportSource( Title $title ) {
144        return new ImportSource( $title, $this->parser, $this->user, $this->headerSuffix );
145    }
146
147    /**
148     * @inheritDoc
149     */
150    public function decideArchiveTitle( Title $source ) {
151        $archiveNameHelper = new ArchiveNameHelper();
152        return $archiveNameHelper->decideArchiveTitle( $source, $this->archiveTitleSuggestions );
153    }
154
155    /**
156     * @inheritDoc
157     */
158    public function getPostprocessor() {
159        return null;
160    }
161
162    /**
163     * @inheritDoc
164     */
165    public function createArchiveCleanupRevisionContent( WikitextContent $content, Title $title ) {
166        $now = new DateTime( "now", new DateTimeZone( "GMT" ) );
167        $arguments = implode( '|', [
168            'from=' . $title->getPrefixedText(),
169            'date=' . $now->format( 'Y-m-d' ),
170        ] );
171
172        $template = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
173        $newWikitext = "{{{$template}|$arguments}}" . "\n\n" . $content->getText();
174
175        return new WikitextContent( $newWikitext );
176    }
177
178    // Public only for unit testing
179
180    /**
181     * Checks whether it meets the applicable subpage rules.  Meant to be overriden by
182     * subclasses that do not have the same requirements
183     *
184     * @param Title $sourceTitle Title to check
185     * @return bool Whether it meets the applicable subpage requirements
186     */
187    public function meetsSubpageRequirements( Title $sourceTitle ) {
188        // Don't allow conversion of sub pages unless it is
189        // a talk page with matching subject page. For example
190        // we will convert User_talk:Foo/bar only if User:Foo/bar
191        // exists, and we will never convert User:Baz/bang.
192        if ( $sourceTitle->isSubpage() &&
193            ( !$sourceTitle->isTalkPage() || !$sourceTitle->getSubjectPage()->exists() )
194        ) {
195            return false;
196        }
197
198        return true;
199    }
200
201    /**
202     * Check whether the given title has one of the templates that should protect it from
203     * being converted.
204     * @param Title $sourceTitle Title to check
205     * @return bool Whether the title has such a template
206     */
207    protected function hasNoConvertTemplate( Title $sourceTitle ) {
208        if ( count( $this->noConvertTemplates ) === 0 ) {
209            return false;
210        }
211
212        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
213        $batch = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch( $this->noConvertTemplates );
214        return (bool)$dbr->newSelectQueryBuilder()
215            ->select( 'tl_from' )
216            ->from( 'templatelinks' )
217            ->where( [
218                'tl_from' => $sourceTitle->getArticleID(),
219                $batch->constructSet( 'tl', $dbr )
220            ] )
221            ->caller( __METHOD__ )
222            ->fetchRow();
223    }
224
225    /**
226     * @inheritDoc
227     */
228    public function shouldConvert( Title $sourceTitle ) {
229        // If we have LiquidThreads filter out any pages with that enabled.  They should
230        // be converted separately.
231        if ( ExtensionRegistry::getInstance()->isLoaded( 'Liquid Threads' ) ) {
232            if ( \LqtDispatch::isLqtPage( $sourceTitle ) ) {
233                $this->logger->info( "Skipping LQT enabled page, conversion must be done with " .
234                    "convertLqtPagesWithProp.php or convertLqtPageOnLocalWiki.php: $sourceTitle" );
235                return false;
236            }
237        }
238
239        if ( !$this->meetsSubpageRequirements( $sourceTitle ) ||
240            $this->hasNoConvertTemplate( $sourceTitle )
241        ) {
242            return false;
243        }
244
245        return true;
246    }
247}