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