Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
93 / 93 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
| WikifunctionsClientStore | |
100.00% |
93 / 93 |
|
100.00% |
7 / 7 |
20 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| insertWikifunctionsUsage | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
| fetchWikifunctionsUsage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| deleteWikifunctionsUsage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| fetchFromZObjectCache | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
| makeFunctionCallCacheKey | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| fetchFromFunctionCallCache | |
100.00% |
46 / 46 |
|
100.00% |
1 / 1 |
12 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * WikiLambda Data Access Object service |
| 4 | * |
| 5 | * @file |
| 6 | * @ingroup Extensions |
| 7 | * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt |
| 8 | * @license MIT |
| 9 | */ |
| 10 | |
| 11 | namespace MediaWiki\Extension\WikiLambda; |
| 12 | |
| 13 | use MediaWiki\Extension\WikiLambda\Cache\MemcachedWrapper; |
| 14 | use MediaWiki\Logger\LoggerFactory; |
| 15 | use MediaWiki\Title\Title; |
| 16 | use Psr\Log\LoggerInterface; |
| 17 | use Wikimedia\Rdbms\IConnectionProvider; |
| 18 | |
| 19 | class WikifunctionsClientStore { |
| 20 | |
| 21 | private IConnectionProvider $dbProvider; |
| 22 | private MemcachedWrapper $objectCache; |
| 23 | |
| 24 | private LoggerInterface $logger; |
| 25 | |
| 26 | public const CLIENT_FUNCTIONCALL_CACHE_KEY_PREFIX = 'WikiLambdaClientFunctionCall'; |
| 27 | |
| 28 | /** |
| 29 | * @param IConnectionProvider $dbProvider |
| 30 | */ |
| 31 | public function __construct( IConnectionProvider $dbProvider ) { |
| 32 | $this->dbProvider = $dbProvider; |
| 33 | |
| 34 | // This can't be injected, as the service container runs before the extension is loaded |
| 35 | $this->objectCache = WikiLambdaServices::getMemcachedWrapper(); |
| 36 | |
| 37 | $this->logger = LoggerFactory::getInstance( 'WikiLambdaClient' ); |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Track in wikifunctionsclient_usage the usage of a function on a page. |
| 42 | * |
| 43 | * NOTE: wfcu_targetPage is stored as getPrefixedText() (e.g. "Template:Foo bar"). |
| 44 | * All readers and writers of this column must use the same representation: |
| 45 | * deleteWikifunctionsUsage() below, and WikifunctionsRecentChangesInsertJob |
| 46 | * which reconstructs a Title via Title::newFromText(). |
| 47 | * |
| 48 | * @param string $targetFunction |
| 49 | * @param Title $targetPage |
| 50 | * @return bool |
| 51 | */ |
| 52 | public function insertWikifunctionsUsage( string $targetFunction, Title $targetPage ): bool { |
| 53 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
| 54 | |
| 55 | $dbw->newInsertQueryBuilder() |
| 56 | ->insertInto( 'wikifunctionsclient_usage' ) |
| 57 | ->row( [ |
| 58 | 'wfcu_targetPage' => $targetPage->getPrefixedText(), |
| 59 | 'wfcu_targetFunction' => $targetFunction, |
| 60 | ] ) |
| 61 | ->set( [ |
| 62 | 'wfcu_targetFunction' => $targetFunction, |
| 63 | ] ) |
| 64 | ->onDuplicateKeyUpdate() |
| 65 | ->uniqueIndexFields( [ |
| 66 | 'wfcu_targetPage', |
| 67 | 'wfcu_targetFunction', |
| 68 | ] ) |
| 69 | // We don't mind duplicates (i.e., the same Function is used twice on the same page) |
| 70 | ->ignore() |
| 71 | ->caller( __METHOD__ )->execute(); |
| 72 | |
| 73 | return (bool)$dbw->affectedRows(); |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * Check in wikifunctionsclient_usage the pages on which a function is used. |
| 78 | * |
| 79 | * @param string $targetFunction |
| 80 | * @return array |
| 81 | */ |
| 82 | public function fetchWikifunctionsUsage( string $targetFunction ): array { |
| 83 | $dbr = $this->dbProvider->getReplicaDatabase(); |
| 84 | return $dbr->newSelectQueryBuilder() |
| 85 | ->select( 'wfcu_targetPage' ) |
| 86 | ->from( 'wikifunctionsclient_usage' ) |
| 87 | ->where( [ 'wfcu_targetFunction' => $targetFunction ] ) |
| 88 | ->caller( __METHOD__ ) |
| 89 | ->fetchFieldValues(); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Drop tracking in wikifunctionsclient_usage of a page. |
| 94 | * |
| 95 | * NOTE: Must match the representation used by insertWikifunctionsUsage() — see the |
| 96 | * note on that method for the full list of readers/writers of wfcu_targetPage. |
| 97 | * |
| 98 | * @param Title $targetPage |
| 99 | * @return void |
| 100 | */ |
| 101 | public function deleteWikifunctionsUsage( Title $targetPage ): void { |
| 102 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
| 103 | |
| 104 | $dbw->newDeleteQueryBuilder() |
| 105 | ->deleteFrom( 'wikifunctionsclient_usage' ) |
| 106 | ->where( [ 'wfcu_targetPage' => $targetPage->getPrefixedText() ] ) |
| 107 | ->caller( __METHOD__ )->execute(); |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Requests the given ZObject from the ZObject cache, given its ZID. |
| 112 | * Returns null if the ZID is not available in the cache. |
| 113 | * |
| 114 | * This is the same as the first part of the ZObjectStore::fetchZObject() method, but without the |
| 115 | * repo-mode follow-up for reading from the wiki, as it's for client wikis. |
| 116 | * |
| 117 | * @param string $zid |
| 118 | * @return ?array |
| 119 | */ |
| 120 | public function fetchFromZObjectCache( string $zid ): ?array { |
| 121 | $cacheKey = $this->objectCache->makeKey( ZObjectStore::ZOBJECT_CACHE_KEY_PREFIX, $zid ); |
| 122 | |
| 123 | $cachedObject = $this->objectCache->get( $cacheKey ); |
| 124 | if ( !$cachedObject ) { |
| 125 | $this->logger->info( __METHOD__ . ' cache miss while fetching {zid}', [ 'zid' => $zid ] ); |
| 126 | return null; |
| 127 | } |
| 128 | |
| 129 | $json = json_decode( $cachedObject, true ); |
| 130 | if ( !$json ) { |
| 131 | $this->logger->warning( __METHOD__ . ' failed parse of cached JSON for {zid}', [ 'zid' => $zid ] ); |
| 132 | } |
| 133 | |
| 134 | // Return successfully parsed JSON, or null |
| 135 | return $json; |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Requests the given Function call from the ZObject cache, given its cache key. |
| 140 | * Returns null if the Function call is not available in the cache. |
| 141 | * |
| 142 | * @param array $functionCall |
| 143 | * @return string |
| 144 | */ |
| 145 | public function makeFunctionCallCacheKey( array $functionCall ): string { |
| 146 | // Note that we can't use ZObjectUtils::makeCacheKeyFromZObject here, as that's repo-mode only. |
| 147 | // This means that this cache key doesn't have the revision IDs of the referenced ZObjects. |
| 148 | return $this->objectCache->makeKey( |
| 149 | self::CLIENT_FUNCTIONCALL_CACHE_KEY_PREFIX, |
| 150 | json_encode( $functionCall ) |
| 151 | ); |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Fetch a Function call result from the cache, given its cache key, or delete it |
| 156 | * if not properly set. |
| 157 | * |
| 158 | * @param string $clientCacheKey |
| 159 | * @return ?array{success:bool, value:?string, type:?string, errorMessageKey:?string} |
| 160 | */ |
| 161 | public function fetchFromFunctionCallCache( string $clientCacheKey ): ?array { |
| 162 | $cachedValue = $this->objectCache->get( $clientCacheKey ); |
| 163 | |
| 164 | if ( !$cachedValue ) { |
| 165 | $this->logger->info( __METHOD__ . ' cache miss while fetching {key}', [ 'key' => $clientCacheKey ] ); |
| 166 | return null; |
| 167 | } |
| 168 | |
| 169 | // Check for corrupted/invalid cache entries and delete them rather than returning them |
| 170 | if ( !is_array( $cachedValue ) ) { |
| 171 | $this->logger->warning( |
| 172 | 'WikiLambda client cache entry for {key} is mal-formed, deleting it', |
| 173 | [ |
| 174 | 'key' => $clientCacheKey |
| 175 | ] |
| 176 | ); |
| 177 | $this->objectCache->delete( $clientCacheKey ); |
| 178 | return null; |
| 179 | } |
| 180 | |
| 181 | if ( !array_key_exists( 'success', $cachedValue ) || !is_bool( $cachedValue['success'] ) ) { |
| 182 | // Corrupted/invalid cache entry; delete it |
| 183 | $this->logger->warning( |
| 184 | 'WikiLambda client cache entry for {key} is missing success boolean, deleting it', |
| 185 | [ |
| 186 | 'key' => $clientCacheKey |
| 187 | ] |
| 188 | ); |
| 189 | $this->objectCache->delete( $clientCacheKey ); |
| 190 | return null; |
| 191 | } |
| 192 | |
| 193 | if ( $cachedValue['success'] ) { |
| 194 | if ( |
| 195 | !array_key_exists( 'value', $cachedValue ) || |
| 196 | !array_key_exists( 'type', $cachedValue ) || |
| 197 | !is_string( $cachedValue['value'] ) || |
| 198 | !is_string( $cachedValue['type'] ) |
| 199 | ) { |
| 200 | // Corrupted/invalid cache entry; delete it |
| 201 | $this->logger->warning( |
| 202 | 'WikiLambda client cache entry for {key} is missing value or type, deleting it', |
| 203 | [ |
| 204 | 'key' => $clientCacheKey |
| 205 | ] |
| 206 | ); |
| 207 | $this->objectCache->delete( $clientCacheKey ); |
| 208 | return null; |
| 209 | } |
| 210 | return $cachedValue; |
| 211 | } |
| 212 | |
| 213 | // We know the success key is false, so we need to check the error message key |
| 214 | |
| 215 | if ( !array_key_exists( 'errorMessageKey', $cachedValue ) || !is_string( $cachedValue['errorMessageKey'] ) ) { |
| 216 | $this->logger->warning( |
| 217 | 'WikiLambda client cache entry for {key} is missing error message key string, deleting it', |
| 218 | [ |
| 219 | 'key' => $clientCacheKey |
| 220 | ] |
| 221 | ); |
| 222 | $this->objectCache->delete( $clientCacheKey ); |
| 223 | return null; |
| 224 | } |
| 225 | |
| 226 | return $cachedValue; |
| 227 | } |
| 228 | } |