Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlickrBlacklist
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 5
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 isBlacklisted
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getBlacklist
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getPhotoIdFromUrl
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getUserIdsFromPhotoId
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\UploadWizard;
4
5use IContextSource;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Title\Title;
8use TextContent;
9
10/**
11 * Checks Flickr images against a blacklist of users
12 */
13class FlickrBlacklist {
14    /**
15     * Regexp to extract photo id (as match group 1) from a static image URL.
16     */
17    private const IMAGE_URL_REGEXP = '!static\.?flickr\.com/[^/]+/([0-9]+)_!';
18
19    /**
20     * Regexp to extract photo id (as match group 1) from a photo page URL.
21     */
22    private const PHOTO_URL_REGEXP = '!flickr\.com/(?:x/t/[^/]+/)?photos/[^/]+/([0-9]+)!';
23
24    /**
25     * An array of the blacklisted Flickr NSIDs and path_aliases.
26     * Used as an in-memory cache to speed successive lookups; null means not yet initialized.
27     * @var array|null
28     */
29    protected static $blacklist = null;
30
31    /**
32     * @var string
33     */
34    protected $flickrApiKey;
35
36    /**
37     * @var string
38     */
39    protected $flickrApiUrl;
40
41    /**
42     * Name of the wiki page which contains the NSID blacklist.
43     *
44     * The page should contain usernames (either the path_alias - the human-readable username
45     * in the URL - or the NSID) separated by whitespace. It is not required to contain both
46     * path_alias and NSID for the same user.
47     *
48     * Lines starting with # are ignored.
49     * @var string
50     */
51    protected $flickrBlacklistPage;
52
53    /**
54     * @var IContextSource
55     */
56    protected $context;
57
58    /**
59     * Sets options based on a config array such as Config::getConfig().
60     * @param array $options an array with 'flickrApiKey', 'flickrApiUrl' and
61     *     'flickrBlacklistPage' keys
62     * @param IContextSource $context
63     */
64    public function __construct( array $options, IContextSource $context ) {
65        $this->flickrApiKey = $options['flickrApiKey'];
66        $this->flickrApiUrl = $options['flickrApiUrl'];
67        $this->flickrBlacklistPage = $options['flickrBlacklistPage'];
68        $this->context = $context;
69    }
70
71    /**
72     * @param string $url
73     * @return bool
74     */
75    public function isBlacklisted( $url ) {
76        $blacklist = $this->getBlacklist();
77
78        $flickrPhotoId = $this->getPhotoIdFromUrl( $url );
79        if ( $flickrPhotoId ) {
80            $userIds = $this->getUserIdsFromPhotoId( $flickrPhotoId );
81            return (bool)array_intersect( $userIds, $blacklist );
82        }
83        // FIXME should we tell the user we did not recognize the URL?
84        return false;
85    }
86
87    /**
88     * Returns the blacklist, which is a non-associative array of user NSIDs and path_aliases
89     * (the name name which can be seen in the pretty URL). For a given user, usually only one
90     * of the NSID and the path_alias will be present; it is the responsibility of the consumers
91     * of the blacklist to check it against both.
92     * @return array
93     */
94    public function getBlacklist() {
95        if ( self::$blacklist === null ) {
96            self::$blacklist = [];
97            if ( $this->flickrBlacklistPage ) {
98                $title = Title::newFromText( $this->flickrBlacklistPage );
99                $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
100                $content = $page->getContent();
101                $text = ( $content instanceof TextContent ) ? $content->getText() : '';
102                $text = preg_replace( '/^\s*#.*$/m', '', $text );
103                preg_match_all( '/\S+/', $text, $match );
104                self::$blacklist = $match[0];
105            }
106        }
107        return self::$blacklist;
108    }
109
110    /**
111     * Takes a Flickr photo page URL or a direct image URL, returns photo id (or false on failure).
112     * @param string $url
113     * @return string|bool
114     */
115    protected function getPhotoIdFromUrl( $url ) {
116        if ( preg_match( self::IMAGE_URL_REGEXP, $url, $matches ) ) {
117            return $matches[1];
118        } elseif ( preg_match( self::PHOTO_URL_REGEXP, $url, $matches ) ) {
119            return $matches[1];
120        } else {
121            return false;
122        }
123    }
124
125    /**
126     * Takes a photo ID, returns owner's NSID and path_alias
127     * (the username which appears in the URL), if available.
128     * @param string $flickrPhotoId
129     * @return array an array containing the NSID first and the path_alias second. The path_alias
130     *     is not guaranteed to exist, in which case the array will have a single item;
131     *     if there is no such photo (or some other error happened), the array will be empty.
132     */
133    protected function getUserIdsFromPhotoId( $flickrPhotoId ) {
134        $userIds = [];
135        $params = [
136            'postData' => [
137                'method' => 'flickr.photos.getInfo',
138                'api_key' => $this->flickrApiKey,
139                'photo_id' => $flickrPhotoId,
140                'format' => 'json',
141                'nojsoncallback' => 1,
142            ],
143        ];
144        $response = MediaWikiServices::getInstance()->getHttpRequestFactory()
145            ->post( $this->flickrApiUrl, $params, __METHOD__ );
146        if ( $response !== false ) {
147            $response = json_decode( $response, true );
148        }
149        if ( isset( $response['photo']['owner']['nsid'] ) ) {
150            $userIds[] = $response['photo']['owner']['nsid'];
151        }
152        // what Flickr calls 'username' can change at any time and so is worthless for blacklisting
153        // path_alias is the username in the pretty URL; once set, it cannot be changed.
154        if ( isset( $response['photo']['owner']['path_alias'] ) ) {
155            $userIds[] = $response['photo']['owner']['path_alias'];
156        }
157        return $userIds;
158    }
159}