Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 70 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
ServiceLinkRecommendationProvider | |
0.00% |
0 / 70 |
|
0.00% |
0 / 2 |
110 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
get | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
90 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\NewcomerTasks\AddLink; |
4 | |
5 | use GrowthExperiments\NewcomerTasks\TaskType\LinkRecommendationTaskType; |
6 | use GrowthExperiments\NewcomerTasks\TaskType\TaskType; |
7 | use MediaWiki\Http\HttpRequestFactory; |
8 | use MediaWiki\Linker\LinkTarget; |
9 | use MediaWiki\Revision\RevisionLookup; |
10 | use MediaWiki\Revision\SlotRecord; |
11 | use MediaWiki\Title\TitleFactory; |
12 | use MediaWiki\Utils\MWTimestamp; |
13 | use RequestContext; |
14 | use StatusValue; |
15 | use Wikimedia\Assert\Assert; |
16 | use WikitextContent; |
17 | |
18 | /** |
19 | * A link recommendation provider that uses the link recommendation service. |
20 | * @see https://wikitech.wikimedia.org/wiki/Add_Link |
21 | */ |
22 | class ServiceLinkRecommendationProvider implements LinkRecommendationProvider { |
23 | |
24 | /** @var TitleFactory */ |
25 | private $titleFactory; |
26 | |
27 | /** @var RevisionLookup */ |
28 | private $revisionLookup; |
29 | |
30 | /** @var HttpRequestFactory */ |
31 | private $httpRequestFactory; |
32 | |
33 | /** @var string */ |
34 | private $url; |
35 | |
36 | /** @var string */ |
37 | private $wikiId; |
38 | |
39 | /** @var string */ |
40 | private $languageCode; |
41 | |
42 | /** @var string|null */ |
43 | private $accessToken; |
44 | |
45 | /** @var int|null Service request timeout in seconds. */ |
46 | private $requestTimeout; |
47 | |
48 | /** |
49 | * @param TitleFactory $titleFactory |
50 | * @param RevisionLookup $revisionLookup |
51 | * @param HttpRequestFactory $httpRequestFactory |
52 | * @param string $url Link recommendation service root URL |
53 | * @param string $wikiId Wiki ID (e.g. "simple", "en") |
54 | * @param string $languageCode the ISO-639 language code (e.g. "az" for Azeri, "en" for English) to use in |
55 | * processing the wikitext |
56 | * @param string|null $accessToken Jwt for authorization with external traffic release of link |
57 | * recommendation service |
58 | * @param int|null $requestTimeout Service request timeout in seconds. |
59 | */ |
60 | public function __construct( |
61 | TitleFactory $titleFactory, |
62 | RevisionLookup $revisionLookup, |
63 | HttpRequestFactory $httpRequestFactory, |
64 | string $url, |
65 | string $wikiId, |
66 | string $languageCode, |
67 | ?string $accessToken, |
68 | ?int $requestTimeout |
69 | ) { |
70 | $this->titleFactory = $titleFactory; |
71 | $this->revisionLookup = $revisionLookup; |
72 | $this->httpRequestFactory = $httpRequestFactory; |
73 | $this->url = $url; |
74 | $this->wikiId = $wikiId; |
75 | $this->languageCode = $languageCode; |
76 | $this->accessToken = $accessToken; |
77 | $this->requestTimeout = $requestTimeout; |
78 | } |
79 | |
80 | /** @inheritDoc */ |
81 | public function get( LinkTarget $title, TaskType $taskType ) { |
82 | Assert::parameterType( LinkRecommendationTaskType::class, $taskType, '$taskType' ); |
83 | /** @var LinkRecommendationTaskType $taskType */'@phan-var LinkRecommendationTaskType $taskType'; |
84 | $title = $this->titleFactory->newFromLinkTarget( $title ); |
85 | $pageId = $title->getArticleID(); |
86 | $titleText = $title->getPrefixedDBkey(); |
87 | $revId = $title->getLatestRevID(); |
88 | |
89 | if ( !$revId ) { |
90 | return StatusValue::newFatal( 'growthexperiments-addlink-pagenotfound', $titleText ); |
91 | } |
92 | $content = $this->revisionLookup->getRevisionById( $revId )->getContent( SlotRecord::MAIN ); |
93 | if ( !$content ) { |
94 | return StatusValue::newFatal( 'growthexperiments-addlink-revdeleted', $revId, $titleText ); |
95 | } elseif ( !( $content instanceof WikitextContent ) ) { |
96 | return StatusValue::newFatal( 'growthexperiments-addlink-wrongmodel', $revId, $titleText ); |
97 | } |
98 | $wikitext = $content->getText(); |
99 | |
100 | // FIXME: Don't hardcode 'wikipedia' project |
101 | // FIXME: Use a less hacky way to get the project/subdomain pair that the API gateway wants. |
102 | $pathArgs = [ 'wikipedia', str_replace( 'wiki', '', $this->wikiId ), $titleText ]; |
103 | $queryArgs = [ |
104 | 'threshold' => $taskType->getMinimumLinkScore(), |
105 | 'max_recommendations' => $taskType->getMaximumLinksPerTask(), |
106 | 'language_code' => $this->languageCode, |
107 | ]; |
108 | $postBodyArgs = [ |
109 | 'pageid' => $pageId, |
110 | 'revid' => $revId, |
111 | 'wikitext' => $wikitext, |
112 | ]; |
113 | if ( $taskType->getExcludedSections() ) { |
114 | $postBodyArgs['sections_to_exclude'] = $taskType->getExcludedSections(); |
115 | } |
116 | $request = $this->httpRequestFactory->create( |
117 | wfAppendQuery( |
118 | $this->url . '/v1/linkrecommendations/' . implode( '/', array_map( 'rawurlencode', $pathArgs ) ), |
119 | $queryArgs |
120 | ), |
121 | [ |
122 | 'method' => 'POST', |
123 | 'postData' => json_encode( $postBodyArgs ), |
124 | 'originalRequest' => RequestContext::getMain()->getRequest(), |
125 | 'timeout' => $this->requestTimeout, |
126 | ], |
127 | __METHOD__ |
128 | ); |
129 | if ( $this->accessToken ) { |
130 | // TODO: Support app authentication with client ID / secret |
131 | // https://api.wikimedia.org/wiki/Documentation/Getting_started/Authentication#App_authentication |
132 | $request->setHeader( 'Authorization', "Bearer $this->accessToken" ); |
133 | } |
134 | $request->setHeader( 'Content-Type', 'application/json' ); |
135 | |
136 | $status = $request->execute(); |
137 | if ( !$status->isOK() ) { |
138 | return $status; |
139 | } |
140 | $response = $request->getContent(); |
141 | |
142 | $data = json_decode( $response, true ); |
143 | if ( $data === null ) { |
144 | return StatusValue::newFatal( 'growthexperiments-addlink-invalidjson', $titleText ); |
145 | } |
146 | if ( array_key_exists( 'error', $data ) ) { |
147 | return StatusValue::newFatal( 'growthexperiments-addlink-serviceerror', |
148 | $titleText, $data['error'] ); |
149 | } |
150 | // TODO validate/process data; compare $data['page_id'] and $data['revid'] |
151 | return new LinkRecommendation( |
152 | $title, |
153 | $pageId, |
154 | $revId, |
155 | LinkRecommendation::getLinksFromArray( $data['links'] ), |
156 | LinkRecommendation::getMetadataFromArray( $data['meta'] + [ |
157 | 'task_timestamp' => MWTimestamp::time(), |
158 | ] ) |
159 | ); |
160 | } |
161 | |
162 | } |