summaryrefslogtreecommitdiff
blob: bec18ef7699257056d42ec662c714dbc29d16e54 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<?php

use MediaWiki\MediaWikiServices;

/**
 * A small wrapper around ObjectCache to manage
 * storing the last time a user has seen notifications
 */
class EchoSeenTime {

	/**
	 * Allowed notification types
	 * @var string[]
	 */
	private static $allowedTypes = [ 'alert', 'message' ];

	/**
	 * @var User
	 */
	private $user;

	/**
	 * @param User $user A logged in user
	 */
	private function __construct( User $user ) {
		$this->user = $user;
	}

	/**
	 * @param User $user
	 * @return EchoSeenTime
	 */
	public static function newFromUser( User $user ) {
		return new self( $user );
	}

	/**
	 * Hold onto a cache for our operations. Static so it can reuse the same
	 * in-process cache in different instances.
	 *
	 * @return BagOStuff
	 */
	private static function cache() {
		static $wrappedCache = null;

		// Use a configurable cache backend (T222851) and wrap it with CachedBagOStuff
		// for an in-process cache (T144534)
		if ( $wrappedCache === null ) {
			$cacheConfig = MediaWikiServices::getInstance()->getMainConfig()->get( 'EchoSeenTimeCacheType' );
			if ( $cacheConfig === null ) {
				// EchoHooks::initEchoExtension sets EchoSeenTimeCacheType to $wgMainStash if it's
				// null, so this can only happen if $wgMainStash is also null
				throw new UnexpectedValueException(
					'Either $wgEchoSeenTimeCacheType or $wgMainStash must be set'
				);
			}
			return new CachedBagOStuff( ObjectCache::getInstance( $cacheConfig ) );
		}

		return $wrappedCache;
	}

	/**
	 * @param string $type Type of seen time to get
	 * @param int $format Format to return time in, defaults to TS_MW
	 * @return string|false Timestamp in specified format, or false if no stored time
	 */
	public function getTime( $type = 'all', $format = TS_MW ) {
		$vals = [];
		if ( $type === 'all' ) {
			foreach ( self::$allowedTypes as $allowed ) {
				// Use TS_MW, then convert later, so max works properly for
				// all formats.
				$vals[] = $this->getTime( $allowed, TS_MW );
			}

			return wfTimestamp( $format, min( $vals ) );
		}

		if ( !$this->validateType( $type ) ) {
			return false;
		}

		$data = self::cache()->get( $this->getMemcKey( $type ) );

		if ( $data === false ) {
			// Check if the user still has it set in their preferences
			$data = $this->user->getOption( 'echo-seen-time', false );
		}

		if ( $data === false ) {
			// We can't remember their real seen time, so reset everything to
			// unseen.
			$data = wfTimestamp( TS_MW, 1 );
		}
		return wfTimestamp( $format, $data );
	}

	/**
	 * Sets the seen time
	 *
	 * @param string $time Time, in TS_MW format
	 * @param string $type Type of seen time to set
	 */
	public function setTime( $time, $type = 'all' ) {
		if ( $type === 'all' ) {
			foreach ( self::$allowedTypes as $allowed ) {
				$this->setTime( $time, $allowed );
			}
			return;
		}

		if ( !$this->validateType( $type ) ) {
			return;
		}

		// Write to the in-memory cache immediately, and defer writing to
		// the real cache
		$key = $this->getMemcKey( $type );
		$cache = self::cache();
		$cache->set( $key, $time, $cache::TTL_YEAR, BagOStuff::WRITE_CACHE_ONLY );
		DeferredUpdates::addCallableUpdate( function () use ( $key, $time, $cache ) {
			$cache->set( $key, $time, $cache::TTL_YEAR );
		} );
	}

	/**
	 * Validate the given type, make sure it is allowed.
	 *
	 * @param string $type Given type
	 * @return bool Type is allowed
	 */
	private function validateType( $type ) {
		return in_array( $type, self::$allowedTypes );
	}

	/**
	 * Build a memcached key.
	 *
	 * @param string $type Given notification type
	 * @return string Memcached key
	 */
	protected function getMemcKey( $type = 'all' ) {
		$localKey = self::cache()->makeKey(
			'echo', 'seen', $type, 'time', $this->user->getId()
		);

		if ( !$this->user->getOption( 'echo-cross-wiki-notifications' ) ) {
			return $localKey;
		}

		$lookup = CentralIdLookup::factory();
		$globalId = $lookup->centralIdFromLocalUser( $this->user, CentralIdLookup::AUDIENCE_RAW );

		if ( !$globalId ) {
			return $localKey;
		}

		return self::cache()->makeGlobalKey(
			'echo', 'seen', $type, 'time', $globalId
		);
	}
}