Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SeenTime
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 7
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newFromUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 cache
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getTime
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 setTime
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 validateType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMemcKey
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\Notifications;
4
5use BagOStuff;
6use CachedBagOStuff;
7use MediaWiki\Deferred\DeferredUpdates;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\User\CentralId\CentralIdLookup;
10use MediaWiki\User\User;
11use ObjectCache;
12use UnexpectedValueException;
13
14/**
15 * A small wrapper around ObjectCache to manage
16 * storing the last time a user has seen notifications
17 */
18class SeenTime {
19
20    /**
21     * Allowed notification types
22     * @var string[]
23     */
24    private static $allowedTypes = [ 'alert', 'message' ];
25
26    /**
27     * @var User
28     */
29    private $user;
30
31    /**
32     * @param User $user A logged in user
33     */
34    private function __construct( User $user ) {
35        $this->user = $user;
36    }
37
38    /**
39     * @param User $user
40     * @return SeenTime
41     */
42    public static function newFromUser( User $user ) {
43        return new self( $user );
44    }
45
46    /**
47     * Hold onto a cache for our operations. Static so it can reuse the same
48     * in-process cache in different instances.
49     *
50     * @return BagOStuff
51     */
52    private static function cache() {
53        static $wrappedCache = null;
54
55        // Use a configurable cache backend (T222851) and wrap it with CachedBagOStuff
56        // for an in-process cache (T144534)
57        if ( $wrappedCache === null ) {
58            $cacheConfig = MediaWikiServices::getInstance()->getMainConfig()->get( 'EchoSeenTimeCacheType' );
59            if ( $cacheConfig === null ) {
60                // Hooks::initEchoExtension sets EchoSeenTimeCacheType to $wgMainStash if it's
61                // null, so this can only happen if $wgMainStash is also null
62                throw new UnexpectedValueException(
63                    'Either $wgEchoSeenTimeCacheType or $wgMainStash must be set'
64                );
65            }
66            return new CachedBagOStuff( ObjectCache::getInstance( $cacheConfig ) );
67        }
68
69        return $wrappedCache;
70    }
71
72    /**
73     * @param string $type Type of seen time to get
74     * @param int $format Format to return time in, defaults to TS_MW
75     * @return string|false Timestamp in specified format, or false if no stored time
76     */
77    public function getTime( $type = 'all', $format = TS_MW ) {
78        $vals = [];
79        if ( $type === 'all' ) {
80            foreach ( self::$allowedTypes as $allowed ) {
81                // Use TS_MW, then convert later, so max works properly for
82                // all formats.
83                $vals[] = $this->getTime( $allowed, TS_MW );
84            }
85
86            return wfTimestamp( $format, min( $vals ) );
87        }
88
89        if ( !$this->validateType( $type ) ) {
90            return false;
91        }
92
93        $data = self::cache()->get( $this->getMemcKey( $type ) );
94
95        if ( $data === false ) {
96            $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
97            // Check if the user still has it set in their preferences
98            $data = $userOptionsLookup->getOption( $this->user, 'echo-seen-time', false );
99        }
100
101        if ( $data === false ) {
102            // We can't remember their real seen time, so reset everything to
103            // unseen.
104            $data = wfTimestamp( TS_MW, 1 );
105        }
106        return wfTimestamp( $format, $data );
107    }
108
109    /**
110     * Sets the seen time
111     *
112     * @param string $time Time, in TS_MW format
113     * @param string $type Type of seen time to set
114     */
115    public function setTime( $time, $type = 'all' ) {
116        if ( $type === 'all' ) {
117            foreach ( self::$allowedTypes as $allowed ) {
118                $this->setTime( $time, $allowed );
119            }
120            return;
121        }
122
123        if ( !$this->validateType( $type ) ) {
124            return;
125        }
126
127        // Write to the in-memory cache immediately, and defer writing to
128        // the real cache
129        $key = $this->getMemcKey( $type );
130        $cache = self::cache();
131        $cache->set( $key, $time, $cache::TTL_YEAR, BagOStuff::WRITE_CACHE_ONLY );
132        DeferredUpdates::addCallableUpdate( static function () use ( $key, $time, $cache ) {
133            $cache->set( $key, $time, $cache::TTL_YEAR );
134        } );
135    }
136
137    /**
138     * Validate the given type, make sure it is allowed.
139     *
140     * @param string $type Given type
141     * @return bool Type is allowed
142     */
143    private function validateType( $type ) {
144        return in_array( $type, self::$allowedTypes );
145    }
146
147    /**
148     * Build a memcached key.
149     *
150     * @param string $type Given notification type
151     * @return string Memcached key
152     */
153    protected function getMemcKey( $type = 'all' ) {
154        $localKey = self::cache()->makeKey(
155            'echo', 'seen', $type, 'time', $this->user->getId()
156        );
157        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
158
159        if ( !$userOptionsLookup->getOption( $this->user, 'echo-cross-wiki-notifications' ) ) {
160            return $localKey;
161        }
162
163        $globalId = MediaWikiServices::getInstance()
164            ->getCentralIdLookup()
165            ->centralIdFromLocalUser( $this->user, CentralIdLookup::AUDIENCE_RAW );
166
167        if ( !$globalId ) {
168            return $localKey;
169        }
170
171        return self::cache()->makeGlobalKey(
172            'echo', 'seen', $type, 'time', $globalId
173        );
174    }
175}