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\Title\TitleValue;
25use MediaWiki\User\UserIdentity;
26use MediaWiki\Utils\MWTimestamp;
27use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
28use Wikimedia\Timestamp\ConvertibleTimestamp;
29
30/**
31 * Representation of a pair of user and title for watchlist entries.
32 *
33 * @author Tim Starling
34 * @author Addshore
35 *
36 * @ingroup Watchlist
37 */
38class WatchedItem {
39    /**
40     * @var LinkTarget|PageIdentity deprecated LinkTarget since 1.36
41     */
42    private $target;
43
44    /**
45     * @var UserIdentity
46     */
47    private $user;
48
49    /**
50     * @var bool|null|string the value of the wl_notificationtimestamp field
51     */
52    private $notificationTimestamp;
53
54    /**
55     * @var ConvertibleTimestamp|null value that determines when a watched item will expire.
56     *  'null' means that there is no expiration.
57     */
58    private $expiry;
59
60    /**
61     * Used to calculate how many days are remaining until a watched item will expire.
62     * Uses a different algorithm from Language::getDurationIntervals for calculating
63     * days remaining in an interval of time
64     *
65     * @since 1.35
66     */
67    private const SECONDS_IN_A_DAY = 86400;
68
69    /**
70     * @param UserIdentity $user
71     * @param LinkTarget|PageIdentity $target deprecated passing LinkTarget since 1.36
72     * @param bool|null|string $notificationTimestamp the value of the wl_notificationtimestamp field
73     * @param null|string $expiry Optional expiry timestamp in any format acceptable to wfTimestamp()
74     */
75    public function __construct(
76        UserIdentity $user,
77        $target,
78        $notificationTimestamp,
79        ?string $expiry = null
80    ) {
81        $this->user = $user;
82        $this->target = $target;
83        $this->notificationTimestamp = $notificationTimestamp;
84
85        // Expiry will be saved in ConvertibleTimestamp
86        $this->expiry = ExpiryDef::normalizeExpiry( $expiry );
87
88        // If the normalization returned 'infinity' then set it as null since they are synonymous
89        if ( $this->expiry === 'infinity' ) {
90            $this->expiry = null;
91        }
92    }
93
94    /**
95     * @since 1.35
96     * @param RecentChange $recentChange
97     * @param UserIdentity $user
98     * @return WatchedItem
99     */
100    public static function newFromRecentChange( RecentChange $recentChange, UserIdentity $user ) {
101        return new self(
102            $user,
103            $recentChange->getTitle(),
104            $recentChange->notificationtimestamp,
105            $recentChange->watchlistExpiry
106        );
107    }
108
109    /**
110     * @return UserIdentity
111     */
112    public function getUserIdentity() {
113        return $this->user;
114    }
115
116    /**
117     * @return LinkTarget
118     * @deprecated since 1.36, use getTarget() instead
119     */
120    public function getLinkTarget() {
121        if ( !$this->target instanceof LinkTarget ) {
122            return TitleValue::newFromPage( $this->target );
123        }
124        return $this->getTarget();
125    }
126
127    /**
128     * @return LinkTarget|PageIdentity deprecated returning LinkTarget since 1.36
129     * @since 1.36
130     */
131    public function getTarget() {
132        return $this->target;
133    }
134
135    /**
136     * Get the notification timestamp of this entry.
137     *
138     * @return bool|null|string
139     */
140    public function getNotificationTimestamp() {
141        return $this->notificationTimestamp;
142    }
143
144    /**
145     * When the watched item will expire.
146     *
147     * @since 1.35
148     * @param int|null $style Given timestamp format to style the ConvertibleTimestamp
149     * @return string|null null or in a format acceptable to ConvertibleTimestamp (TS_* constants).
150     *  Default is TS_MW format.
151     */
152    public function getExpiry( ?int $style = TS_MW ) {
153        return $this->expiry instanceof ConvertibleTimestamp
154            ? $this->expiry->getTimestamp( $style )
155            : $this->expiry;
156    }
157
158    /**
159     * Has the watched item expired?
160     *
161     * @since 1.35
162     *
163     * @return bool
164     */
165    public function isExpired(): bool {
166        $expiry = $this->getExpiry();
167        if ( $expiry === null ) {
168            return false;
169        }
170        return $expiry < ConvertibleTimestamp::now();
171    }
172
173    /**
174     * Get days remaining until a watched item expires.
175     *
176     * @since 1.35
177     *
178     * @return int|null days remaining or null if no expiration is present
179     */
180    public function getExpiryInDays(): ?int {
181        return self::calculateExpiryInDays( $this->getExpiry() );
182    }
183
184    /**
185     * Get the number of days remaining until the given expiry time.
186     *
187     * @since 1.35
188     *
189     * @param string|null $expiry The expiry to calculate from, in any format
190     * supported by MWTimestamp::convert().
191     *
192     * @return int|null The remaining number of days or null if $expiry is null.
193     */
194    public static function calculateExpiryInDays( ?string $expiry ): ?int {
195        if ( $expiry === null ) {
196            return null;
197        }
198
199        $unixTimeExpiry = (int)MWTimestamp::convert( TS_UNIX, $expiry );
200        $diffInSeconds = $unixTimeExpiry - (int)wfTimestamp( TS_UNIX );
201        $diffInDays = $diffInSeconds / self::SECONDS_IN_A_DAY;
202
203        if ( $diffInDays < 1 ) {
204            return 0;
205        }
206
207        return (int)ceil( $diffInDays );
208    }
209
210    /**
211     * Get days remaining until a watched item expires as a text.
212     *
213     * @since 1.35
214     * @param MessageLocalizer $msgLocalizer
215     * @param bool $isDropdownOption Whether the text is being displayed as a dropdown option.
216     *             The text is different as a dropdown option from when it is used in other
217     *             places as a watchlist indicator.
218     * @return string days remaining text and '' if no expiration is present
219     */
220    public function getExpiryInDaysText( MessageLocalizer $msgLocalizer, $isDropdownOption = false ): string {
221        $expiryInDays = $this->getExpiryInDays();
222        if ( $expiryInDays === null ) {
223            return '';
224        }
225
226        if ( $expiryInDays < 1 ) {
227            if ( $isDropdownOption ) {
228                return $msgLocalizer->msg( 'watchlist-expiry-hours-left' )->text();
229            }
230            return $msgLocalizer->msg( 'watchlist-expiring-hours-full-text' )->text();
231        }
232
233        if ( $isDropdownOption ) {
234            return $msgLocalizer->msg( 'watchlist-expiry-days-left', [ $expiryInDays ] )->text();
235        }
236
237        return $msgLocalizer->msg( 'watchlist-expiring-days-full-text', [ $expiryInDays ] )->text();
238    }
239}