Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.53% covered (success)
98.53%
67 / 68
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RemoteSourceFileEditDeleteAction
98.53% covered (success)
98.53%
67 / 68
83.33% covered (warning)
83.33%
5 / 6
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 manualTemplateFallback
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addNowCommonsToSource
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
3
 deleteSourceFile
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 successMessage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace FileImporter\Remote\MediaWiki;
4
5use FileImporter\Data\ImportPlan;
6use FileImporter\Interfaces\PostImportHandler;
7use FileImporter\Services\WikidataTemplateLookup;
8use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
9use MediaWiki\User\User;
10use MediaWiki\Utils\UrlUtils;
11use Psr\Log\LoggerInterface;
12use Psr\Log\NullLogger;
13use StatusValue;
14use Wikimedia\Stats\NullStatsdDataFactory;
15
16/**
17 * Delete the source file, or edit to add the {{NowCommons}} template.
18 *
19 * @license GPL-2.0-or-later
20 */
21class RemoteSourceFileEditDeleteAction implements PostImportHandler {
22
23    private const STATSD_SOURCE_DELETE_FAIL = 'FileImporter.import.postImport.delete.failed';
24    private const STATSD_SOURCE_DELETE_SUCCESS = 'FileImporter.import.postImport.delete.successful';
25    private const STATSD_SOURCE_EDIT_FAIL = 'FileImporter.import.postImport.edit.failed';
26    private const STATSD_SOURCE_EDIT_SUCCESS = 'FileImporter.import.postImport.edit.successful';
27
28    private PostImportHandler $fallbackHandler;
29    private WikidataTemplateLookup $templateLookup;
30    private RemoteApiActionExecutor $remoteAction;
31    private UrlUtils $urlUtils;
32    private LoggerInterface $logger;
33    private StatsdDataFactoryInterface $statsd;
34
35    public function __construct(
36        PostImportHandler $fallbackHandler,
37        WikidataTemplateLookup $templateLookup,
38        RemoteApiActionExecutor $remoteAction,
39        UrlUtils $urlUtils,
40        ?LoggerInterface $logger = null,
41        ?StatsdDataFactoryInterface $statsd = null
42    ) {
43        $this->fallbackHandler = $fallbackHandler;
44        $this->templateLookup = $templateLookup;
45        $this->remoteAction = $remoteAction;
46        $this->urlUtils = $urlUtils;
47        $this->logger = $logger ?? new NullLogger();
48        $this->statsd = $statsd ?? new NullStatsdDataFactory();
49    }
50
51    /**
52     * @inheritDoc
53     */
54    public function execute( ImportPlan $importPlan, User $user ): StatusValue {
55        if ( $importPlan->getAutomateSourceWikiDelete() ) {
56            return $this->deleteSourceFile( $importPlan, $user );
57        } elseif ( $importPlan->getAutomateSourceWikiCleanUp() ) {
58            return $this->addNowCommonsToSource( $importPlan, $user );
59        } else {
60            // Note this may also be triggered if the above methods fail.
61            return $this->manualTemplateFallback( $importPlan, $user );
62        }
63    }
64
65    private function manualTemplateFallback(
66        ImportPlan $importPlan,
67        User $user,
68        ?string $warningMsg = null
69    ): StatusValue {
70        $status = $this->fallbackHandler->execute( $importPlan, $user );
71        if ( $warningMsg ) {
72            $status->warning( $warningMsg );
73        }
74        return $status;
75    }
76
77    private function addNowCommonsToSource( ImportPlan $importPlan, User $user ): StatusValue {
78        $templateName = $this->templateLookup->fetchNowCommonsLocalTitle(
79            $importPlan->getDetails()->getSourceUrl()
80        );
81        // This should be unreachable because the user can't click the checkbox in this case. But we
82        // know this from a POST request, which might be altered or simply outdated.
83        // Note: This intentionally doesn't allow a template with the name "0".
84        if ( !$templateName ) {
85            return $this->successMessage();
86        }
87
88        $sourceUrl = $importPlan->getDetails()->getSourceUrl();
89        $summary = wfMessage(
90            'fileimporter-cleanup-summary',
91            $this->urlUtils->expandIRI( $importPlan->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ) ?? ''
92        )->inLanguage( $importPlan->getDetails()->getPageLanguage() )->text();
93        $text = "\n{{" . wfEscapeWikiText( $templateName ) . '|' .
94            wfEscapeWikiText( $importPlan->getTitle()->getText() ) . '}}';
95
96        $status = $this->remoteAction->executeEditAction(
97            $sourceUrl,
98            $user,
99            $importPlan->getOriginalTitle()->getPrefixedText(),
100            [ 'appendtext' => $text ],
101            $summary
102        );
103
104        if ( $status->isGood() ) {
105            $this->statsd->increment( self::STATSD_SOURCE_EDIT_SUCCESS );
106            return $this->successMessage();
107        } else {
108            $this->logger->error( __METHOD__ . ' failed to do post import edit.' );
109            $this->statsd->increment( self::STATSD_SOURCE_EDIT_FAIL );
110
111            return $this->manualTemplateFallback(
112                $importPlan,
113                $user,
114                'fileimporter-cleanup-failed'
115            );
116        }
117    }
118
119    private function deleteSourceFile( ImportPlan $importPlan, User $user ): StatusValue {
120        $sourceUrl = $importPlan->getDetails()->getSourceUrl();
121        $summary = wfMessage(
122            'fileimporter-delete-summary',
123            $this->urlUtils->expandIRI( $importPlan->getTitle()->getFullURL( '', false, PROTO_CANONICAL ) ) ?? ''
124        )->inLanguage( $importPlan->getDetails()->getPageLanguage() )->text();
125
126        $status = $this->remoteAction->executeDeleteAction(
127            $sourceUrl,
128            $user,
129            $importPlan->getOriginalTitle()->getPrefixedText(),
130            $summary
131        );
132
133        if ( $status->isGood() ) {
134            $this->statsd->increment( self::STATSD_SOURCE_DELETE_SUCCESS );
135            return $this->successMessage();
136        } else {
137            $this->logger->error( __METHOD__ . ' failed to do post import delete.' );
138            $this->statsd->increment( self::STATSD_SOURCE_DELETE_FAIL );
139
140            $status = $this->successMessage();
141            $status->warning(
142                'fileimporter-delete-failed',
143                $sourceUrl->getHost(),
144                $sourceUrl->getUrl()
145            );
146            return $status;
147        }
148    }
149
150    private function successMessage(): StatusValue {
151        return StatusValue::newGood( 'fileimporter-imported-success-banner' );
152    }
153
154}