Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.45% covered (warning)
70.45%
31 / 44
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
WatchedItem
70.45% covered (warning)
70.45%
31 / 44
45.45% covered (danger)
45.45%
5 / 11
32.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 newFromRecentChange
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getUserIdentity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLinkTarget
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNotificationTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExpiry
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isExpired
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getExpiryInDays
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 calculateExpiryInDays
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getExpiryInDaysText
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Watchlist
20 */
21
22use MediaWiki\Linker\LinkTarget;
23use MediaWiki\Page\PageIdentity;
24use MediaWiki\User\UserIdentity;
25use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
26use Wikimedia\Timestamp\ConvertibleTimestamp;
27
28/**
29 * Representation of a pair of user and title for watchlist entries.
30 *
31 * @author Tim Starling
32 * @author Addshore
33 *
34 * @ingroup Watchlist
35 */
36class WatchedItem {
37    /**
38     * @var LinkTarget|PageIdentity deprecated LinkTarget since 1.36
39     */
40    private $target;
41
42    /**
43     * @var UserIdentity
44     */
45    private $user;
46
47    /**
48     * @var bool|null|string the value of the wl_notificationtimestamp field
49     */
50    private $notificationTimestamp;
51
52    /**
53     * @var ConvertibleTimestamp|null value that determines when a watched item will expire.
54     *  'null' means that there is no expiration.
55     */
56    private $expiry;
57
58    /**
59     * Used to calculate how many days are remaining until a watched item will expire.
60     * Uses a different algorithm from Language::getDurationIntervals for calculating
61     * days remaining in an interval of time
62     *
63     * @since 1.35
64     */
65    private const SECONDS_IN_A_DAY = 86400;
66
67    /**
68     * @param UserIdentity $user
69     * @param LinkTarget|PageIdentity $target deprecated passing LinkTarget since 1.36
70     * @param bool|null|string $notificationTimestamp the value of the wl_notificationtimestamp field
71     * @param null|string $expiry Optional expiry timestamp in any format acceptable to wfTimestamp()
72     */
73    public function __construct(
74        UserIdentity $user,
75        $target,
76        $notificationTimestamp,
77        ?string $expiry = null
78    ) {
79        $this->user = $user;
80        $this->target = $target;
81        $this->notificationTimestamp = $notificationTimestamp;
82
83        // Expiry will be saved in ConvertibleTimestamp
84        $this->expiry = ExpiryDef::normalizeExpiry( $expiry );
85
86        // If the normalization returned 'infinity' then set it as null since they are synonymous
87        if ( $this->expiry === 'infinity' ) {
88            $this->expiry = null;
89        }
90    }
91
92    /**
93     * @since 1.35
94     * @param RecentChange $recentChange
95     * @param UserIdentity $user
96     * @return WatchedItem
97     */
98    public static function newFromRecentChange( RecentChange $recentChange, UserIdentity $user ) {
99        return new self(
100            $user,
101            $recentChange->getTitle(),
102            $recentChange->notificationtimestamp,
103            $recentChange->watchlistExpiry
104        );
105    }
106
107    /**
108     * @return UserIdentity
109     */
110    public function getUserIdentity() {
111        return $this->user;
112    }
113
114    /**
115     * @return LinkTarget
116     * @deprecated since 1.36, use getTarget() instead
117     */
118    public function getLinkTarget() {
119        if ( !$this->target instanceof LinkTarget ) {
120            return TitleValue::newFromPage( $this->target );
121        }
122        return $this->getTarget();
123    }
124
125    /**
126     * @return LinkTarget|PageIdentity deprecated returning LinkTarget since 1.36
127     * @since 1.36
128     */
129    public function getTarget() {
130        return $this->target;
131    }
132
133    /**
134     * Get the notification timestamp of this entry.
135     *
136     * @return bool|null|string
137     */
138    public function getNotificationTimestamp() {
139        return $this->notificationTimestamp;
140    }
141
142    /**
143     * When the watched item will expire.
144     *
145     * @since 1.35
146     * @param int|null $style Given timestamp format to style the ConvertibleTimestamp
147     * @return string|null null or in a format acceptable to ConvertibleTimestamp (TS_* constants).
148     *  Default is TS_MW format.
149     */
150    public function getExpiry( ?int $style = TS_MW ) {
151        return $this->expiry instanceof ConvertibleTimestamp
152            ? $this->expiry->getTimestamp( $style )
153            : $this->expiry;
154    }
155
156    /**
157     * Has the watched item expired?
158     *
159     * @since 1.35
160     *
161     * @return bool
162     */
163    public function isExpired(): bool {
164        $expiry = $this->getExpiry();
165        if ( $expiry === null ) {
166            return false;
167        }
168        return $expiry < ConvertibleTimestamp::now();
169    }
170
171    /**
172     * Get days remaining until a watched item expires.
173     *
174     * @since 1.35
175     *
176     * @return int|null days remaining or null if no expiration is present
177     */
178    public function getExpiryInDays(): ?int {
179        return self::calculateExpiryInDays( $this->getExpiry() );
180    }
181
182    /**
183     * Get the number of days remaining until the given expiry time.
184     *
185     * @since 1.35
186     *
187     * @param string|null $expiry The expiry to calculate from, in any format
188     * supported by MWTimestamp::convert().
189     *
190     * @return int|null The remaining number of days or null if $expiry is null.
191     */
192    public static function calculateExpiryInDays( ?string $expiry ): ?int {
193        if ( $expiry === null ) {
194            return null;
195        }
196
197        $unixTimeExpiry = (int)MWTimestamp::convert( TS_UNIX, $expiry );
198        $diffInSeconds = $unixTimeExpiry - (int)wfTimestamp( TS_UNIX );
199        $diffInDays = $diffInSeconds / self::SECONDS_IN_A_DAY;
200
201        if ( $diffInDays < 1 ) {
202            return 0;
203        }
204
205        return (int)ceil( $diffInDays );
206    }
207
208    /**
209     * Get days remaining until a watched item expires as a text.
210     *
211     * @since 1.35
212     * @param MessageLocalizer $msgLocalizer
213     * @param bool $isDropdownOption Whether the text is being displayed as a dropdown option.
214     *             The text is different as a dropdown option from when it is used in other
215     *             places as a watchlist indicator.
216     * @return string days remaining text and '' if no expiration is present
217     */
218    public function getExpiryInDaysText( MessageLocalizer $msgLocalizer, $isDropdownOption = false ): string {
219        $expiryInDays = $this->getExpiryInDays();
220        if ( $expiryInDays === null ) {
221            return '';
222        }
223
224        if ( $expiryInDays < 1 ) {
225            if ( $isDropdownOption ) {
226                return $msgLocalizer->msg( 'watchlist-expiry-hours-left' )->text();
227            }
228            return $msgLocalizer->msg( 'watchlist-expiring-hours-full-text' )->text();
229        }
230
231        if ( $isDropdownOption ) {
232            return $msgLocalizer->msg( 'watchlist-expiry-days-left', [ $expiryInDays ] )->text();
233        }
234
235        return $msgLocalizer->msg( 'watchlist-expiring-days-full-text', [ $expiryInDays ] )->text();
236    }
237}