PageRenderTime 43ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/MessageBlobStore.php

https://bitbucket.org/brunodefraine/mediawiki
PHP | 358 lines | 197 code | 42 blank | 119 comment | 19 complexity | ce371cdaad10031505e20131193455d6 MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, LGPL-3.0
  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. * @author Roan Kattouw
  19. * @author Trevor Parscal
  20. */
  21. /**
  22. * This class provides access to the resource message blobs storage used by
  23. * the ResourceLoader.
  24. *
  25. * A message blob is a JSON object containing the interface messages for a
  26. * certain resource in a certain language. These message blobs are cached
  27. * in the msg_resource table and automatically invalidated when one of their
  28. * consistuent messages or the resource itself is changed.
  29. */
  30. class MessageBlobStore {
  31. /**
  32. * Get the message blobs for a set of modules
  33. *
  34. * @param $resourceLoader ResourceLoader object
  35. * @param $modules array Array of module objects keyed by module name
  36. * @param $lang string Language code
  37. * @return array An array mapping module names to message blobs
  38. */
  39. public static function get( ResourceLoader $resourceLoader, $modules, $lang ) {
  40. wfProfileIn( __METHOD__ );
  41. if ( !count( $modules ) ) {
  42. wfProfileOut( __METHOD__ );
  43. return array();
  44. }
  45. // Try getting from the DB first
  46. $blobs = self::getFromDB( $resourceLoader, array_keys( $modules ), $lang );
  47. // Generate blobs for any missing modules and store them in the DB
  48. $missing = array_diff( array_keys( $modules ), array_keys( $blobs ) );
  49. foreach ( $missing as $name ) {
  50. $blob = self::insertMessageBlob( $name, $modules[$name], $lang );
  51. if ( $blob ) {
  52. $blobs[$name] = $blob;
  53. }
  54. }
  55. wfProfileOut( __METHOD__ );
  56. return $blobs;
  57. }
  58. /**
  59. * Generate and insert a new message blob. If the blob was already
  60. * present, it is not regenerated; instead, the preexisting blob
  61. * is fetched and returned.
  62. *
  63. * @param $name String: module name
  64. * @param $module ResourceLoaderModule object
  65. * @param $lang String: language code
  66. * @return mixed Message blob or false if the module has no messages
  67. */
  68. public static function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
  69. $blob = self::generateMessageBlob( $module, $lang );
  70. if ( !$blob ) {
  71. return false;
  72. }
  73. $dbw = wfGetDB( DB_MASTER );
  74. $success = $dbw->insert( 'msg_resource', array(
  75. 'mr_lang' => $lang,
  76. 'mr_resource' => $name,
  77. 'mr_blob' => $blob,
  78. 'mr_timestamp' => $dbw->timestamp()
  79. ),
  80. __METHOD__,
  81. array( 'IGNORE' )
  82. );
  83. if ( $success ) {
  84. if ( $dbw->affectedRows() == 0 ) {
  85. // Blob was already present, fetch it
  86. $blob = $dbw->selectField( 'msg_resource', 'mr_blob', array(
  87. 'mr_resource' => $name,
  88. 'mr_lang' => $lang,
  89. ),
  90. __METHOD__
  91. );
  92. } else {
  93. // Update msg_resource_links
  94. $rows = array();
  95. foreach ( $module->getMessages() as $key ) {
  96. $rows[] = array(
  97. 'mrl_resource' => $name,
  98. 'mrl_message' => $key
  99. );
  100. }
  101. $dbw->insert( 'msg_resource_links', $rows,
  102. __METHOD__, array( 'IGNORE' )
  103. );
  104. }
  105. }
  106. return $blob;
  107. }
  108. /**
  109. * Update the message blob for a given module in a given language
  110. *
  111. * @param $name String: module name
  112. * @param $module ResourceLoaderModule object
  113. * @param $lang String: language code
  114. * @return String Regenerated message blob, or null if there was no blob for the given module/language pair
  115. */
  116. public static function updateModule( $name, ResourceLoaderModule $module, $lang ) {
  117. $dbw = wfGetDB( DB_MASTER );
  118. $row = $dbw->selectRow( 'msg_resource', 'mr_blob',
  119. array( 'mr_resource' => $name, 'mr_lang' => $lang ),
  120. __METHOD__
  121. );
  122. if ( !$row ) {
  123. return null;
  124. }
  125. // Save the old and new blobs for later
  126. $oldBlob = $row->mr_blob;
  127. $newBlob = self::generateMessageBlob( $module, $lang );
  128. $newRow = array(
  129. 'mr_resource' => $name,
  130. 'mr_lang' => $lang,
  131. 'mr_blob' => $newBlob,
  132. 'mr_timestamp' => $dbw->timestamp()
  133. );
  134. $dbw->replace( 'msg_resource',
  135. array( array( 'mr_resource', 'mr_lang' ) ),
  136. $newRow, __METHOD__
  137. );
  138. // Figure out which messages were added and removed
  139. $oldMessages = array_keys( FormatJson::decode( $oldBlob, true ) );
  140. $newMessages = array_keys( FormatJson::decode( $newBlob, true ) );
  141. $added = array_diff( $newMessages, $oldMessages );
  142. $removed = array_diff( $oldMessages, $newMessages );
  143. // Delete removed messages, insert added ones
  144. if ( $removed ) {
  145. $dbw->delete( 'msg_resource_links', array(
  146. 'mrl_resource' => $name,
  147. 'mrl_message' => $removed
  148. ), __METHOD__
  149. );
  150. }
  151. $newLinksRows = array();
  152. foreach ( $added as $message ) {
  153. $newLinksRows[] = array(
  154. 'mrl_resource' => $name,
  155. 'mrl_message' => $message
  156. );
  157. }
  158. if ( $newLinksRows ) {
  159. $dbw->insert( 'msg_resource_links', $newLinksRows, __METHOD__,
  160. array( 'IGNORE' ) // just in case
  161. );
  162. }
  163. return $newBlob;
  164. }
  165. /**
  166. * Update a single message in all message blobs it occurs in.
  167. *
  168. * @param $key String: message key
  169. */
  170. public static function updateMessage( $key ) {
  171. $dbw = wfGetDB( DB_MASTER );
  172. // Keep running until the updates queue is empty.
  173. // Due to update conflicts, the queue might not be emptied
  174. // in one iteration.
  175. $updates = null;
  176. do {
  177. $updates = self::getUpdatesForMessage( $key, $updates );
  178. foreach ( $updates as $k => $update ) {
  179. // Update the row on the condition that it
  180. // didn't change since we fetched it by putting
  181. // the timestamp in the WHERE clause.
  182. $success = $dbw->update( 'msg_resource',
  183. array(
  184. 'mr_blob' => $update['newBlob'],
  185. 'mr_timestamp' => $dbw->timestamp() ),
  186. array(
  187. 'mr_resource' => $update['resource'],
  188. 'mr_lang' => $update['lang'],
  189. 'mr_timestamp' => $update['timestamp'] ),
  190. __METHOD__
  191. );
  192. // Only requeue conflicted updates.
  193. // If update() returned false, don't retry, for
  194. // fear of getting into an infinite loop
  195. if ( !( $success && $dbw->affectedRows() == 0 ) ) {
  196. // Not conflicted
  197. unset( $updates[$k] );
  198. }
  199. }
  200. } while ( count( $updates ) );
  201. // No need to update msg_resource_links because we didn't add
  202. // or remove any messages, we just changed their contents.
  203. }
  204. public static function clear() {
  205. // TODO: Give this some more thought
  206. // TODO: Is TRUNCATE better?
  207. $dbw = wfGetDB( DB_MASTER );
  208. $dbw->delete( 'msg_resource', '*', __METHOD__ );
  209. $dbw->delete( 'msg_resource_links', '*', __METHOD__ );
  210. }
  211. /**
  212. * Create an update queue for updateMessage()
  213. *
  214. * @param $key String: message key
  215. * @param $prevUpdates Array: updates queue to refresh or null to build a fresh update queue
  216. * @return Array: updates queue
  217. */
  218. private static function getUpdatesForMessage( $key, $prevUpdates = null ) {
  219. $dbw = wfGetDB( DB_MASTER );
  220. if ( is_null( $prevUpdates ) ) {
  221. // Fetch all blobs referencing $key
  222. $res = $dbw->select(
  223. array( 'msg_resource', 'msg_resource_links' ),
  224. array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
  225. array( 'mrl_message' => $key, 'mr_resource=mrl_resource' ),
  226. __METHOD__
  227. );
  228. } else {
  229. // Refetch the blobs referenced by $prevUpdates
  230. // Reorganize the (resource, lang) pairs in the format
  231. // expected by makeWhereFrom2d()
  232. $twoD = array();
  233. foreach ( $prevUpdates as $update ) {
  234. $twoD[$update['resource']][$update['lang']] = true;
  235. }
  236. $res = $dbw->select( 'msg_resource',
  237. array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ),
  238. $dbw->makeWhereFrom2d( $twoD, 'mr_resource', 'mr_lang' ),
  239. __METHOD__
  240. );
  241. }
  242. // Build the new updates queue
  243. $updates = array();
  244. foreach ( $res as $row ) {
  245. $updates[] = array(
  246. 'resource' => $row->mr_resource,
  247. 'lang' => $row->mr_lang,
  248. 'timestamp' => $row->mr_timestamp,
  249. 'newBlob' => self::reencodeBlob( $row->mr_blob, $key, $row->mr_lang )
  250. );
  251. }
  252. return $updates;
  253. }
  254. /**
  255. * Reencode a message blob with the updated value for a message
  256. *
  257. * @param $blob String: message blob (JSON object)
  258. * @param $key String: message key
  259. * @param $lang String: language code
  260. * @return Message blob with $key replaced with its new value
  261. */
  262. private static function reencodeBlob( $blob, $key, $lang ) {
  263. $decoded = FormatJson::decode( $blob, true );
  264. $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
  265. return FormatJson::encode( (object)$decoded );
  266. }
  267. /**
  268. * Get the message blobs for a set of modules from the database.
  269. * Modules whose blobs are not in the database are silently dropped.
  270. *
  271. * @param $resourceLoader ResourceLoader object
  272. * @param $modules Array of module names
  273. * @param $lang String: language code
  274. * @return array Array mapping module names to blobs
  275. */
  276. private static function getFromDB( ResourceLoader $resourceLoader, $modules, $lang ) {
  277. global $wgCacheEpoch;
  278. $retval = array();
  279. $dbr = wfGetDB( DB_SLAVE );
  280. $res = $dbr->select( 'msg_resource',
  281. array( 'mr_blob', 'mr_resource', 'mr_timestamp' ),
  282. array( 'mr_resource' => $modules, 'mr_lang' => $lang ),
  283. __METHOD__
  284. );
  285. foreach ( $res as $row ) {
  286. $module = $resourceLoader->getModule( $row->mr_resource );
  287. if ( !$module ) {
  288. // This shouldn't be possible
  289. throw new MWException( __METHOD__ . ' passed an invalid module name' );
  290. }
  291. // Update the module's blobs if the set of messages changed or if the blob is
  292. // older than $wgCacheEpoch
  293. if ( array_keys( FormatJson::decode( $row->mr_blob, true ) ) !== array_values( array_unique( $module->getMessages() ) ) ||
  294. wfTimestamp( TS_MW, $row->mr_timestamp ) <= $wgCacheEpoch ) {
  295. $retval[$row->mr_resource] = self::updateModule( $row->mr_resource, $module, $lang );
  296. } else {
  297. $retval[$row->mr_resource] = $row->mr_blob;
  298. }
  299. }
  300. return $retval;
  301. }
  302. /**
  303. * Generate the message blob for a given module in a given language.
  304. *
  305. * @param $module ResourceLoaderModule object
  306. * @param $lang String: language code
  307. * @return String: JSON object
  308. */
  309. private static function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
  310. $messages = array();
  311. foreach ( $module->getMessages() as $key ) {
  312. $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) );
  313. }
  314. return FormatJson::encode( (object)$messages );
  315. }
  316. }