Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
62.07% |
144 / 232 |
|
33.33% |
7 / 21 |
CRAP | |
0.00% |
0 / 1 |
UrlShortenerUtils | |
62.07% |
144 / 232 |
|
33.33% |
7 / 21 |
372.85 | |
0.00% |
0 / 1 |
maybeCreateShortCode | |
18.18% |
8 / 44 |
|
0.00% |
0 / 1 |
43.05 | |||
normalizeUrl | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 | |||
convertToProtocol | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getURL | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
3.04 | |||
isURLDeleted | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
2.00 | |||
deleteURL | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
restoreURL | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
cartesianProduct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
getShortcodeVariants | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
purgeCdnId | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
purgeCdn | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getPrimaryDB | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getReplicaDB | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
makeUrl | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getAllowedDomainsRegex | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
validateUrl | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
90 | |||
encodeId | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
decodeId | |
91.30% |
21 / 23 |
|
0.00% |
0 / 1 |
9.05 | |||
shouldShortenUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getQrCode | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
5.00 | |||
getQrCodeInternal | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Functions used for decoding/encoding URLs |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @author Yuvi Panda, http://yuvi.in |
8 | * @copyright © 2014 Yuvaraj Pandian (yuvipanda@gmail.com) |
9 | * @license Apache-2.0 |
10 | */ |
11 | |
12 | namespace MediaWiki\Extension\UrlShortener; |
13 | |
14 | use Endroid\QrCode\Builder\Builder; |
15 | use Endroid\QrCode\Encoding\Encoding; |
16 | use Endroid\QrCode\Writer\Result\ResultInterface; |
17 | use Endroid\QrCode\Writer\SvgWriter; |
18 | use MediaWiki\Deferred\CdnCacheUpdate; |
19 | use MediaWiki\Deferred\DeferredUpdates; |
20 | use MediaWiki\MediaWikiServices; |
21 | use MediaWiki\SpecialPage\SpecialPage; |
22 | use MediaWiki\Status\Status; |
23 | use MediaWiki\User\User; |
24 | use Message; |
25 | use Wikimedia\Rdbms\IDatabase; |
26 | use Wikimedia\Rdbms\IReadableDatabase; |
27 | |
28 | class UrlShortenerUtils { |
29 | |
30 | /** |
31 | * How long to cache valid redirects in CDN (one month) |
32 | * |
33 | * @var int |
34 | */ |
35 | public const CACHE_TTL_VALID = 2592000; |
36 | |
37 | /** |
38 | * How long to cache invalid redirects in CDN (fifteen minutes) |
39 | * |
40 | * @var int |
41 | */ |
42 | public const CACHE_TTL_INVALID = 900; |
43 | |
44 | /** @var int[] */ |
45 | public static $decodeMap; |
46 | |
47 | /** |
48 | * Gets the short code for the given URL, creating if it doesn't |
49 | * have one already. |
50 | * |
51 | * If it already exists in cache or the database, just returns that. |
52 | * Otherwise, a new shortcode entry is created and returned. |
53 | * |
54 | * @param string $url URL to encode |
55 | * @param User $user User requesting the url, for rate limiting |
56 | * @return Status Status with value of base36 encoded shortcode that refers to the $url |
57 | */ |
58 | public static function maybeCreateShortCode( string $url, User $user ): Status { |
59 | global $wgUrlShortenerUrlSizeLimit; |
60 | $url = self::normalizeUrl( $url ); |
61 | |
62 | if ( $user->getBlock() ) { |
63 | return Status::newFatal( 'urlshortener-blocked' ); |
64 | } |
65 | |
66 | global $wgUrlShortenerReadOnly; |
67 | if ( $wgUrlShortenerReadOnly ) { |
68 | // All code paths should already have checked for this, |
69 | // but lets be on the safe side. |
70 | return Status::newFatal( 'urlshortener-disabled' ); |
71 | } |
72 | |
73 | if ( mb_strlen( $url ) > $wgUrlShortenerUrlSizeLimit ) { |
74 | return Status::newFatal( |
75 | wfMessage( 'urlshortener-url-too-long' )->numParams( $wgUrlShortenerUrlSizeLimit ) |
76 | ); |
77 | } |
78 | |
79 | if ( $user->pingLimiter( 'urlshortcode' ) ) { |
80 | return Status::newFatal( 'urlshortener-ratelimit' ); |
81 | } |
82 | |
83 | $dbr = self::getReplicaDB(); |
84 | $row = $dbr->newSelectQueryBuilder() |
85 | ->select( [ 'usc_id', 'usc_deleted' ] ) |
86 | ->from( 'urlshortcodes' ) |
87 | ->where( [ 'usc_url_hash' => md5( $url ) ] ) |
88 | ->caller( __METHOD__ )->fetchRow(); |
89 | if ( $row !== false ) { |
90 | if ( $row->usc_deleted ) { |
91 | return Status::newFatal( 'urlshortener-deleted' ); |
92 | } |
93 | return Status::newGood( [ |
94 | 'url' => self::encodeId( $row->usc_id ), |
95 | 'alt' => self::encodeId( $row->usc_id, true ) |
96 | ] ); |
97 | } |
98 | |
99 | $dbw = self::getPrimaryDB(); |
100 | $dbw->newInsertQueryBuilder() |
101 | ->insertInto( 'urlshortcodes' ) |
102 | ->ignore() |
103 | ->row( [ 'usc_url' => $url, 'usc_url_hash' => md5( $url ) ] ) |
104 | ->caller( __METHOD__ )->execute(); |
105 | |
106 | if ( $dbw->affectedRows() ) { |
107 | $id = $dbw->insertId(); |
108 | } else { |
109 | // Raced out; get the winning ID |
110 | $id = $dbw->newSelectQueryBuilder() |
111 | ->select( 'usc_id' ) |
112 | // ignore snapshot |
113 | ->lockInShareMode() |
114 | ->from( 'urlshortcodes' ) |
115 | ->where( [ 'usc_url_hash' => md5( $url ) ] ) |
116 | ->caller( __METHOD__ )->fetchField(); |
117 | } |
118 | |
119 | // In case our CDN cached an earlier 404/error, purge it |
120 | self::purgeCdnId( $id ); |
121 | |
122 | return Status::newGood( [ |
123 | 'url' => self::encodeId( $id ), |
124 | 'alt' => self::encodeId( $id, true ) |
125 | ] ); |
126 | } |
127 | |
128 | /** |
129 | * Normalizes URL into a somewhat canonical form, including: |
130 | * * protocol to HTTP |
131 | * * from its `/w/index.php?title=$1` form to `/wiki/$1`. |
132 | * |
133 | * @param string $url might be encoded or decoded (raw user input) |
134 | * @return string URL that is saved in DB and used in Location header |
135 | */ |
136 | public static function normalizeUrl( string $url ): string { |
137 | global $wgArticlePath; |
138 | // First, force the protocol to HTTP, we'll convert |
139 | // it to a different one when redirecting |
140 | $url = self::convertToProtocol( $url, PROTO_HTTP ); |
141 | |
142 | $url = trim( $url ); |
143 | |
144 | // TODO: We should ideally decode/encode the URL for normalization, |
145 | // but we don't want to double-encode, nor unencode the URL that |
146 | // is directly provided by users (see test cases) |
147 | // So for now, just replace spaces with %20, as that's safe in all cases |
148 | $url = str_replace( ' ', '%20', $url ); |
149 | |
150 | // If the wiki is using an article path (e.g. /wiki/$1) try |
151 | // and convert plain index.php?title=$1 URLs to the canonical form |
152 | $parsed = wfParseUrl( $url ); |
153 | if ( !isset( $parsed['path'] ) ) { |
154 | // T220718: Ensure each URL has a / after the domain name |
155 | $parsed['path'] = '/'; |
156 | } |
157 | if ( $wgArticlePath !== false && isset( $parsed['query'] ) ) { |
158 | $query = wfCgiToArray( $parsed['query'] ); |
159 | if ( count( $query ) === 1 && isset( $query['title'] ) && $parsed['path'] === wfScript() ) { |
160 | $parsed['path'] = str_replace( '$1', $query['title'], $wgArticlePath ); |
161 | unset( $parsed['query'] ); |
162 | } |
163 | } |
164 | $url = wfAssembleUrl( $parsed ); |
165 | |
166 | return $url; |
167 | } |
168 | |
169 | /** |
170 | * Converts a possibly protocol'd url to the one specified |
171 | * |
172 | * @param string $url |
173 | * @param string|int $proto PROTO_* constant |
174 | * @return string |
175 | */ |
176 | public static function convertToProtocol( string $url, $proto = PROTO_RELATIVE ): string { |
177 | $parsed = wfParseUrl( $url ); |
178 | unset( $parsed['scheme'] ); |
179 | $parsed['delimiter'] = '//'; |
180 | |
181 | return wfExpandUrl( wfAssembleUrl( $parsed ), $proto ); |
182 | } |
183 | |
184 | /** |
185 | * Retrieves a URL for the given shortcode, or false if there's none. |
186 | * |
187 | * @param string $shortCode |
188 | * @param string|int|null $proto PROTO_* constant |
189 | * @return string|false |
190 | */ |
191 | public static function getURL( string $shortCode, $proto = PROTO_RELATIVE ) { |
192 | $id = self::decodeId( $shortCode ); |
193 | if ( $id === false ) { |
194 | return false; |
195 | } |
196 | |
197 | $dbr = self::getReplicaDB(); |
198 | $url = $dbr->newSelectQueryBuilder() |
199 | ->select( 'usc_url' ) |
200 | ->from( 'urlshortcodes' ) |
201 | ->where( [ 'usc_id' => $id, 'usc_deleted' => 0 ] ) |
202 | ->caller( __METHOD__ )->fetchField(); |
203 | |
204 | if ( $url === false ) { |
205 | return false; |
206 | } |
207 | |
208 | return self::convertToProtocol( $url, $proto ); |
209 | } |
210 | |
211 | /** |
212 | * Whether a URL is deleted or not |
213 | * |
214 | * @param string $shortCode |
215 | * @return bool |
216 | */ |
217 | public static function isURLDeleted( string $shortCode ): bool { |
218 | $id = self::decodeId( $shortCode ); |
219 | if ( $id === false ) { |
220 | return false; |
221 | } |
222 | |
223 | $dbr = self::getReplicaDB(); |
224 | $url = $dbr->newSelectQueryBuilder() |
225 | ->select( 'usc_url' ) |
226 | ->from( 'urlshortcodes' ) |
227 | ->where( [ 'usc_id' => $id, 'usc_deleted' => 1 ] ) |
228 | ->caller( __METHOD__ )->fetchField(); |
229 | |
230 | return $url !== false; |
231 | } |
232 | |
233 | /** |
234 | * Mark a URL as deleted |
235 | * |
236 | * @param string $shortcode |
237 | * @return bool False if the $shortCode was invalid |
238 | */ |
239 | public static function deleteURL( string $shortcode ): bool { |
240 | $id = self::decodeId( $shortcode ); |
241 | if ( $id === false ) { |
242 | return false; |
243 | } |
244 | |
245 | $dbw = self::getPrimaryDB(); |
246 | $dbw->newUpdateQueryBuilder() |
247 | ->update( 'urlshortcodes' ) |
248 | ->set( [ 'usc_deleted' => 1 ] ) |
249 | ->where( [ 'usc_id' => $id ] ) |
250 | ->caller( __METHOD__ )->execute(); |
251 | |
252 | self::purgeCdnId( $id ); |
253 | |
254 | return true; |
255 | } |
256 | |
257 | /** |
258 | * Mark a URL as undeleted |
259 | * |
260 | * @param string $shortcode |
261 | * @return bool False if the $shortCode was invalid |
262 | */ |
263 | public static function restoreURL( string $shortcode ): bool { |
264 | $id = self::decodeId( $shortcode ); |
265 | if ( $id === false ) { |
266 | return false; |
267 | } |
268 | |
269 | $dbw = self::getPrimaryDB(); |
270 | $dbw->newUpdateQueryBuilder() |
271 | ->update( 'urlshortcodes' ) |
272 | ->set( [ 'usc_deleted' => 0 ] ) |
273 | ->where( [ 'usc_id' => $id ] ) |
274 | ->caller( __METHOD__ )->execute(); |
275 | |
276 | self::purgeCdnId( $id ); |
277 | |
278 | return true; |
279 | } |
280 | |
281 | /** |
282 | * Compute the Cartesian product of a list of sets |
283 | * |
284 | * @param array[] $sets List of sets |
285 | * @return array[] |
286 | */ |
287 | public static function cartesianProduct( array $sets ): array { |
288 | if ( !$sets ) { |
289 | return [ [] ]; |
290 | } |
291 | |
292 | $set = array_shift( $sets ); |
293 | $productSet = self::cartesianProduct( $sets ); |
294 | |
295 | $result = []; |
296 | foreach ( $set as $val ) { |
297 | foreach ( $productSet as $p ) { |
298 | array_unshift( $p, $val ); |
299 | $result[] = $p; |
300 | } |
301 | } |
302 | |
303 | return $result; |
304 | } |
305 | |
306 | /** |
307 | * Compute all shortcode variants by expanding wgUrlShortenerIdMapping |
308 | * |
309 | * @param string $shortcode |
310 | * @return string[] |
311 | */ |
312 | public static function getShortcodeVariants( string $shortcode ): array { |
313 | global $wgUrlShortenerIdMapping; |
314 | |
315 | // Reverse the character alias mapping |
316 | $targetToVariants = []; |
317 | foreach ( $wgUrlShortenerIdMapping as $variant => $target ) { |
318 | $targetToVariants[ $target ] ??= []; |
319 | $targetToVariants[ $target ][] = (string)$variant; |
320 | } |
321 | |
322 | // Build a set for each character of possible variants |
323 | $sets = []; |
324 | $chars = str_split( $shortcode ); |
325 | foreach ( $chars as $char ) { |
326 | $set = $targetToVariants[ $char ] ?? []; |
327 | $set[] = $char; |
328 | $sets[] = $set; |
329 | } |
330 | |
331 | // Cartesian product to get all combinations |
332 | $productSet = self::cartesianProduct( $sets ); |
333 | |
334 | // Flatten to strings |
335 | return array_map( static function ( $set ) { |
336 | return implode( '', $set ); |
337 | }, $productSet ); |
338 | } |
339 | |
340 | /** |
341 | * If configured, purge CDN for the given ID |
342 | * |
343 | * @param int $id |
344 | */ |
345 | public static function purgeCdnId( int $id ): void { |
346 | global $wgUseCdn; |
347 | if ( $wgUseCdn ) { |
348 | $codes = array_merge( |
349 | self::getShortcodeVariants( self::encodeId( $id ) ), |
350 | self::getShortcodeVariants( self::encodeId( $id, true ) ) |
351 | ); |
352 | foreach ( $codes as $code ) { |
353 | self::purgeCdn( $code ); |
354 | } |
355 | } |
356 | } |
357 | |
358 | /** |
359 | * If configured, purge CDN for the given shortcode |
360 | * |
361 | * @param string $shortcode |
362 | */ |
363 | private static function purgeCdn( string $shortcode ): void { |
364 | global $wgUseCdn; |
365 | if ( $wgUseCdn ) { |
366 | $update = new CdnCacheUpdate( [ self::makeUrl( $shortcode ) ] ); |
367 | DeferredUpdates::addUpdate( $update, DeferredUpdates::PRESEND ); |
368 | } |
369 | } |
370 | |
371 | public static function getPrimaryDB(): IDatabase { |
372 | return MediaWikiServices::getInstance() |
373 | ->getDBLoadBalancerFactory() |
374 | ->getPrimaryDatabase( 'virtual-urlshortener' ); |
375 | } |
376 | |
377 | public static function getReplicaDB(): IReadableDatabase { |
378 | return MediaWikiServices::getInstance() |
379 | ->getDBLoadBalancerFactory() |
380 | ->getReplicaDatabase( 'virtual-urlshortener' ); |
381 | } |
382 | |
383 | /** |
384 | * Create a fully qualified short URL for the given shortcode. |
385 | * |
386 | * @param string $shortCode base64 shortcode to generate URL For. |
387 | * @return string The fully qualified URL |
388 | */ |
389 | public static function makeUrl( string $shortCode ): string { |
390 | global $wgUrlShortenerTemplate, $wgUrlShortenerServer, $wgServer; |
391 | |
392 | if ( $wgUrlShortenerServer === false ) { |
393 | $wgUrlShortenerServer = $wgServer; |
394 | } |
395 | |
396 | if ( !is_string( $wgUrlShortenerTemplate ) ) { |
397 | $urlTemplate = SpecialPage::getTitleFor( 'UrlRedirector', '$1' )->getFullUrl(); |
398 | } else { |
399 | $urlTemplate = $wgUrlShortenerServer . $wgUrlShortenerTemplate; |
400 | } |
401 | $url = str_replace( '$1', $shortCode, $urlTemplate ); |
402 | |
403 | // Make sure the URL is fully qualified |
404 | return wfExpandUrl( $url ); |
405 | } |
406 | |
407 | /** |
408 | * Coalesce the regex of allowed domains into a single string regex. |
409 | * |
410 | * @return string Regex of allowed domains |
411 | */ |
412 | public static function getAllowedDomainsRegex(): string { |
413 | global $wgUrlShortenerAllowedDomains, $wgServer; |
414 | if ( $wgUrlShortenerAllowedDomains === false ) { |
415 | // Allowed Domains not configured, default to wgServer |
416 | $serverParts = wfParseUrl( $wgServer ); |
417 | $allowedDomains = preg_quote( $serverParts['host'], '/' ); |
418 | } else { |
419 | // Collapse the allowed domains into a single string, so we have to run regex check only once |
420 | $allowedDomains = implode( '|', array_map( |
421 | static function ( $item ) { |
422 | return '^' . $item . '$'; |
423 | }, |
424 | $wgUrlShortenerAllowedDomains |
425 | ) ); |
426 | } |
427 | |
428 | return $allowedDomains; |
429 | } |
430 | |
431 | /** |
432 | * Validates a given URL to see if it is allowed to be used to create a short URL |
433 | * |
434 | * @param string $url Url to Validate |
435 | * @return bool|Message true if it is valid, or error Message object if invalid |
436 | */ |
437 | public static function validateUrl( string $url ) { |
438 | global $wgUrlShortenerAllowArbitraryPorts; |
439 | |
440 | $urlParts = wfParseUrl( $url ); |
441 | if ( $urlParts === false ) { |
442 | return wfMessage( 'urlshortener-error-malformed-url' ); |
443 | } else { |
444 | if ( isset( $urlParts['port'] ) && !$wgUrlShortenerAllowArbitraryPorts ) { |
445 | if ( $urlParts['port'] === 80 || $urlParts['port'] === 443 ) { |
446 | unset( $urlParts['port'] ); |
447 | } else { |
448 | return wfMessage( 'urlshortener-error-badports' ); |
449 | } |
450 | } |
451 | |
452 | if ( isset( $urlParts['user'] ) || isset( $urlParts['pass'] ) ) { |
453 | return wfMessage( 'urlshortener-error-nouserpass' ); |
454 | } |
455 | |
456 | $domain = $urlParts['host']; |
457 | |
458 | if ( preg_match( '/' . self::getAllowedDomainsRegex() . '/', $domain ) === 1 ) { |
459 | return true; |
460 | } |
461 | |
462 | return wfMessage( 'urlshortener-error-disallowed-url' )->params( htmlentities( $domain ) ); |
463 | } |
464 | } |
465 | |
466 | /** |
467 | * Encode an integer into a compact string representation. This is basically |
468 | * a generalisation of base_convert(). |
469 | * |
470 | * @param int $x |
471 | * @param bool $alt Provide an alternate string representation |
472 | * @return string |
473 | */ |
474 | public static function encodeId( int $x, bool $alt = false ): string { |
475 | global $wgUrlShortenerIdSet, $wgUrlShortenerAltPrefix; |
476 | $s = ''; |
477 | $n = strlen( $wgUrlShortenerIdSet ); |
478 | while ( $x ) { |
479 | $remainder = $x % $n; |
480 | $x = ( $x - $remainder ) / $n; |
481 | $s = $wgUrlShortenerIdSet[$alt ? $n - 1 - $remainder : $remainder] . $s; |
482 | } |
483 | return $alt ? $wgUrlShortenerAltPrefix . $s : $s; |
484 | } |
485 | |
486 | /** |
487 | * Decode a compact string to produce an integer, or false if the input is invalid. |
488 | * |
489 | * @param string $s |
490 | * @return int|false |
491 | */ |
492 | public static function decodeId( string $s ) { |
493 | global $wgUrlShortenerIdSet, $wgUrlShortenerIdMapping, $wgUrlShortenerAltPrefix; |
494 | |
495 | if ( $s === '' ) { |
496 | return false; |
497 | } |
498 | |
499 | $alt = false; |
500 | if ( $s[0] === $wgUrlShortenerAltPrefix ) { |
501 | $s = substr( $s, 1 ); |
502 | $alt = true; |
503 | } |
504 | |
505 | $n = strlen( $wgUrlShortenerIdSet ); |
506 | if ( self::$decodeMap === null ) { |
507 | self::$decodeMap = []; |
508 | for ( $i = 0; $i < $n; $i++ ) { |
509 | self::$decodeMap[$wgUrlShortenerIdSet[$i]] = $i; |
510 | } |
511 | foreach ( $wgUrlShortenerIdMapping as $k => $v ) { |
512 | self::$decodeMap[$k] = self::$decodeMap[$v]; |
513 | } |
514 | } |
515 | |
516 | $x = 0; |
517 | for ( $i = 0, $len = strlen( $s ); $i < $len; $i++ ) { |
518 | $x *= $n; |
519 | if ( isset( self::$decodeMap[$s[$i]] ) ) { |
520 | $val = self::$decodeMap[$s[$i]]; |
521 | $x += $alt ? |
522 | $n - 1 - $val : |
523 | $val; |
524 | } else { |
525 | return false; |
526 | } |
527 | } |
528 | return $x; |
529 | } |
530 | |
531 | /** |
532 | * Given the context of whether we want a QR code, should the URL be shortened? |
533 | * |
534 | * @param bool $qrCode |
535 | * @param string $url |
536 | * @param int $limit The value of $wgUrlShortenerQrCodeShortenLimit |
537 | * @return bool |
538 | */ |
539 | public static function shouldShortenUrl( bool $qrCode, string $url, int $limit ): bool { |
540 | return !$qrCode || strlen( $url ) > $limit; |
541 | } |
542 | |
543 | /** |
544 | * Build a QR code for the given URL. If the URL is longer than $limit in bytes, |
545 | * it will first be shortened to prevent the QR code density from being too high. |
546 | * |
547 | * @param string $url |
548 | * @param int $limit The value of $wgUrlShortenerQrCodeShortenLimit |
549 | * @param User $user User requesting the url, for rate limiting |
550 | * @param bool $dataUri Return 'qrcode' as a data URI instead of XML. |
551 | * @return Status Status with 'qrcode' (XML of the SVG) and if applicable, the shortened 'url' and 'alt'. |
552 | */ |
553 | public static function getQrCode( string $url, int $limit, User $user, bool $dataUri = false ): Status { |
554 | $shortUrlCode = null; |
555 | $shortUrlCodeAlt = null; |
556 | if ( self::shouldShortenUrl( true, $url, $limit ) ) { |
557 | $status = self::maybeCreateShortCode( $url, $user ); |
558 | if ( !$status->isOK() ) { |
559 | return $status; |
560 | } |
561 | $shortUrlCode = $status->getValue()['url']; |
562 | $shortUrlCodeAlt = $status->getValue()['alt']; |
563 | $url = self::makeUrl( $shortUrlCode ); |
564 | } else { |
565 | $url = self::normalizeUrl( $url ); |
566 | } |
567 | $qrCode = self::getQrCodeInternal( $url ); |
568 | $res = [ |
569 | 'qrcode' => $dataUri ? $qrCode->getDataUri() : $qrCode->getString(), |
570 | ]; |
571 | if ( $shortUrlCode ) { |
572 | $res['url'] = $shortUrlCode; |
573 | $res['alt'] = $shortUrlCodeAlt; |
574 | } |
575 | return Status::newGood( $res ); |
576 | } |
577 | |
578 | private static function getQrCodeInternal( string $url ): ResultInterface { |
579 | return Builder::create() |
580 | ->writer( new SvgWriter() ) |
581 | ->writerOptions( [] ) |
582 | ->data( $url ) |
583 | ->encoding( new Encoding( 'UTF-8' ) ) |
584 | ->size( 300 ) |
585 | ->margin( 10 ) |
586 | ->build(); |
587 | } |
588 | } |