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