PageRenderTime 55ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/Gadgets/includes/MediaWikiGadgetsDefinitionRepo.php

https://bitbucket.org/andersus/querytalogo
PHP | 247 lines | 169 code | 32 blank | 46 comment | 27 complexity | c936e52f046cb33fa37784455cf42010 MD5 | raw file
Possible License(s): LGPL-3.0, MPL-2.0-no-copyleft-exception, JSON, MIT, CC0-1.0, BSD-3-Clause, Apache-2.0, BSD-2-Clause, LGPL-2.1, GPL-2.0
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. use Wikimedia\Rdbms\Database;
  4. /**
  5. * Gadgets repo powered by MediaWiki:Gadgets-definition
  6. */
  7. class MediaWikiGadgetsDefinitionRepo extends GadgetRepo {
  8. const CACHE_VERSION = 2;
  9. private $definitionCache;
  10. public function getGadget( $id ) {
  11. $gadgets = $this->loadGadgets();
  12. if ( !isset( $gadgets[$id] ) ) {
  13. throw new InvalidArgumentException( "No gadget registered for '$id'" );
  14. }
  15. return $gadgets[$id];
  16. }
  17. public function getGadgetIds() {
  18. $gadgets = $this->loadGadgets();
  19. if ( $gadgets ) {
  20. return array_keys( $gadgets );
  21. } else {
  22. return [];
  23. }
  24. }
  25. /**
  26. * Purge the definitions cache, for example if MediaWiki:Gadgets-definition
  27. * was edited.
  28. */
  29. public function purgeDefinitionCache() {
  30. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  31. $cache->touchCheckKey( $this->getCheckKey() );
  32. }
  33. private function getCheckKey() {
  34. return wfMemcKey( 'gadgets-definition', Gadget::GADGET_CLASS_VERSION, self::CACHE_VERSION );
  35. }
  36. /**
  37. * Loads list of gadgets and returns it as associative array of sections with gadgets
  38. * e.g. [ 'sectionnname1' => [ $gadget1, $gadget2 ],
  39. * 'sectionnname2' => [ $gadget3 ] ];
  40. * @return array|bool Gadget array or false on failure
  41. */
  42. protected function loadGadgets() {
  43. if ( $this->definitionCache !== null ) {
  44. return $this->definitionCache; // process cache hit
  45. }
  46. // Ideally $t1Cache is APC, and $wanCache is memcached
  47. $t1Cache = ObjectCache::getLocalServerInstance( 'hash' );
  48. $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  49. $key = $this->getCheckKey();
  50. // (a) Check the tier 1 cache
  51. $value = $t1Cache->get( $key );
  52. // Check if it passes a blind TTL check (avoids I/O)
  53. if ( $value && ( microtime( true ) - $value['time'] ) < 10 ) {
  54. $this->definitionCache = $value['gadgets']; // process cache
  55. return $this->definitionCache;
  56. }
  57. // Cache generated after the "check" time should be up-to-date
  58. $ckTime = $wanCache->getCheckKeyTime( $key ) + WANObjectCache::HOLDOFF_TTL;
  59. if ( $value && $value['time'] > $ckTime ) {
  60. $this->definitionCache = $value['gadgets']; // process cache
  61. return $this->definitionCache;
  62. }
  63. // (b) Fetch value from WAN cache or regenerate if needed.
  64. // This is hit occasionally and more so when the list changes.
  65. $us = $this;
  66. $value = $wanCache->getWithSetCallback(
  67. $key,
  68. Gadget::CACHE_TTL,
  69. function ( $old, &$ttl, &$setOpts ) use ( $us ) {
  70. $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
  71. $now = microtime( true );
  72. $gadgets = $us->fetchStructuredList();
  73. if ( $gadgets === false ) {
  74. $ttl = WANObjectCache::TTL_UNCACHEABLE;
  75. }
  76. return [ 'gadgets' => $gadgets, 'time' => $now ];
  77. },
  78. [ 'checkKeys' => [ $key ], 'lockTSE' => 300 ]
  79. );
  80. // Update the tier 1 cache as needed
  81. if ( $value['gadgets'] !== false && $value['time'] > $ckTime ) {
  82. // Set a modest TTL to keep the WAN key in cache
  83. $t1Cache->set( $key, $value, mt_rand( 300, 600 ) );
  84. }
  85. $this->definitionCache = $value['gadgets'];
  86. return $this->definitionCache;
  87. }
  88. /**
  89. * Fetch list of gadgets and returns it as associative array of sections with gadgets
  90. * e.g. [ $name => $gadget1, etc. ]
  91. * @param string $forceNewText Injected text of MediaWiki:gadgets-definition [optional]
  92. * @return array|bool
  93. */
  94. public function fetchStructuredList( $forceNewText = null ) {
  95. if ( $forceNewText === null ) {
  96. // T157210: avoid using wfMessage() to avoid staleness due to cache layering
  97. $title = Title::makeTitle( NS_MEDIAWIKI, 'Gadgets-definition' );
  98. $rev = Revision::newFromTitle( $title );
  99. if ( !$rev || !$rev->getContent() || $rev->getContent()->isEmpty() ) {
  100. return false; // don't cache
  101. }
  102. $g = $rev->getContent()->getNativeData();
  103. } else {
  104. $g = $forceNewText;
  105. }
  106. $gadgets = $this->listFromDefinition( $g );
  107. if ( !count( $gadgets ) ) {
  108. return false; // don't cache; Bug 37228
  109. }
  110. $source = $forceNewText !== null ? 'input text' : 'MediaWiki:Gadgets-definition';
  111. wfDebug( __METHOD__ . ": $source parsed, cache entry should be updated\n" );
  112. return $gadgets;
  113. }
  114. /**
  115. * Generates a structured list of Gadget objects from a definition
  116. *
  117. * @param string $definition
  118. * @return Gadget[] List of Gadget objects indexed by the gadget's name.
  119. */
  120. private function listFromDefinition( $definition ) {
  121. $definition = preg_replace( '/<!--.*?-->/s', '', $definition );
  122. $lines = preg_split( '/(\r\n|\r|\n)+/', $definition );
  123. $gadgets = [];
  124. $section = '';
  125. foreach ( $lines as $line ) {
  126. $m = [];
  127. if ( preg_match( '/^==+ *([^*:\s|]+?)\s*==+\s*$/', $line, $m ) ) {
  128. $section = $m[1];
  129. } else {
  130. $gadget = $this->newFromDefinition( $line, $section );
  131. if ( $gadget ) {
  132. $gadgets[$gadget->getName()] = $gadget;
  133. }
  134. }
  135. }
  136. return $gadgets;
  137. }
  138. /**
  139. * Creates an instance of this class from definition in MediaWiki:Gadgets-definition
  140. * @param string $definition Gadget definition
  141. * @param string $category
  142. * @return Gadget|bool Instance of Gadget class or false if $definition is invalid
  143. */
  144. public function newFromDefinition( $definition, $category ) {
  145. $m = [];
  146. if ( !preg_match(
  147. '/^\*+ *([a-zA-Z](?:[-_:.\w\d ]*[a-zA-Z0-9])?)(\s*\[.*?\])?\s*((\|[^|]*)+)\s*$/',
  148. $definition,
  149. $m
  150. ) ) {
  151. return false;
  152. }
  153. // NOTE: the gadget name is used as part of the name of a form field,
  154. // and must follow the rules defined in https://www.w3.org/TR/html4/types.html#type-cdata
  155. // Also, title-normalization applies.
  156. $info = [ 'category' => $category ];
  157. $info['name'] = trim( str_replace( ' ', '_', $m[1] ) );
  158. // If the name is too long, then RL will throw an MWException when
  159. // we try to register the module
  160. if ( !Gadget::isValidGadgetID( $info['name'] ) ) {
  161. return false;
  162. }
  163. $info['definition'] = $definition;
  164. $options = trim( $m[2], ' []' );
  165. foreach ( preg_split( '/\s*\|\s*/', $options, -1, PREG_SPLIT_NO_EMPTY ) as $option ) {
  166. $arr = preg_split( '/\s*=\s*/', $option, 2 );
  167. $option = $arr[0];
  168. if ( isset( $arr[1] ) ) {
  169. $params = explode( ',', $arr[1] );
  170. $params = array_map( 'trim', $params );
  171. } else {
  172. $params = [];
  173. }
  174. switch ( $option ) {
  175. case 'ResourceLoader':
  176. $info['resourceLoaded'] = true;
  177. break;
  178. case 'dependencies':
  179. $info['dependencies'] = $params;
  180. break;
  181. case 'peers':
  182. $info['peers'] = $params;
  183. break;
  184. case 'rights':
  185. $info['requiredRights'] = $params;
  186. break;
  187. case 'hidden':
  188. $info['hidden'] = true;
  189. break;
  190. case 'skins':
  191. $info['requiredSkins'] = $params;
  192. break;
  193. case 'default':
  194. $info['onByDefault'] = true;
  195. break;
  196. case 'targets':
  197. $info['targets'] = $params;
  198. break;
  199. case 'type':
  200. // Single value, not a list
  201. $info['type'] = isset( $params[0] ) ? $params[0] : '';
  202. break;
  203. }
  204. }
  205. foreach ( preg_split( '/\s*\|\s*/', $m[3], -1, PREG_SPLIT_NO_EMPTY ) as $page ) {
  206. $page = "MediaWiki:Gadget-$page";
  207. if ( preg_match( '/\.js/', $page ) ) {
  208. $info['scripts'][] = $page;
  209. } elseif ( preg_match( '/\.css/', $page ) ) {
  210. $info['styles'][] = $page;
  211. }
  212. }
  213. return new Gadget( $info );
  214. }
  215. }