AjaxResponse.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. <?php
  2. /**
  3. * Response handler for Ajax requests.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Ajax
  22. */
  23. use MediaWiki\MediaWikiServices;
  24. /**
  25. * Handle responses for Ajax requests (send headers, print
  26. * content, that sort of thing)
  27. *
  28. * @ingroup Ajax
  29. */
  30. class AjaxResponse {
  31. /**
  32. * Number of seconds to get the response cached by a proxy
  33. * @var int $mCacheDuration
  34. */
  35. private $mCacheDuration;
  36. /**
  37. * HTTP header Content-Type
  38. * @var string $mContentType
  39. */
  40. private $mContentType;
  41. /**
  42. * Disables output. Can be set by calling $AjaxResponse->disable()
  43. * @var bool $mDisabled
  44. */
  45. private $mDisabled;
  46. /**
  47. * Date for the HTTP header Last-modified
  48. * @var string|bool $mLastModified
  49. */
  50. private $mLastModified;
  51. /**
  52. * HTTP response code
  53. * @var int|string $mResponseCode
  54. */
  55. private $mResponseCode;
  56. /**
  57. * HTTP Vary header
  58. * @var string $mVary
  59. */
  60. private $mVary;
  61. /**
  62. * Content of our HTTP response
  63. * @var string $mText
  64. */
  65. private $mText;
  66. /**
  67. * @var Config
  68. */
  69. private $mConfig;
  70. /**
  71. * @param string|null $text
  72. * @param Config|null $config
  73. */
  74. function __construct( $text = null, Config $config = null ) {
  75. $this->mCacheDuration = null;
  76. $this->mVary = null;
  77. $this->mConfig = $config ?: MediaWikiServices::getInstance()->getMainConfig();
  78. $this->mDisabled = false;
  79. $this->mText = '';
  80. $this->mResponseCode = 200;
  81. $this->mLastModified = false;
  82. $this->mContentType = 'application/x-wiki';
  83. if ( $text ) {
  84. $this->addText( $text );
  85. }
  86. }
  87. /**
  88. * Set the number of seconds to get the response cached by a proxy
  89. * @param int $duration
  90. */
  91. function setCacheDuration( $duration ) {
  92. $this->mCacheDuration = $duration;
  93. }
  94. /**
  95. * Set the HTTP Vary header
  96. * @param string $vary
  97. */
  98. function setVary( $vary ) {
  99. $this->mVary = $vary;
  100. }
  101. /**
  102. * Set the HTTP response code
  103. * @param int|string $code
  104. */
  105. function setResponseCode( $code ) {
  106. $this->mResponseCode = $code;
  107. }
  108. /**
  109. * Set the HTTP header Content-Type
  110. * @param string $type
  111. */
  112. function setContentType( $type ) {
  113. $this->mContentType = $type;
  114. }
  115. /**
  116. * Disable output.
  117. */
  118. function disable() {
  119. $this->mDisabled = true;
  120. }
  121. /**
  122. * Add content to the response
  123. * @param string $text
  124. */
  125. function addText( $text ) {
  126. if ( !$this->mDisabled && $text ) {
  127. $this->mText .= $text;
  128. }
  129. }
  130. /**
  131. * Output text
  132. */
  133. function printText() {
  134. if ( !$this->mDisabled ) {
  135. print $this->mText;
  136. }
  137. }
  138. /**
  139. * Construct the header and output it
  140. */
  141. function sendHeaders() {
  142. if ( $this->mResponseCode ) {
  143. // For back-compat, it is supported that mResponseCode be a string like " 200 OK"
  144. // (with leading space and the status message after). Cast response code to an integer
  145. // to take advantage of PHP's conversion rules which will turn " 200 OK" into 200.
  146. // https://www.php.net/manual/en/language.types.string.php#language.types.string.conversion
  147. $n = intval( trim( $this->mResponseCode ) );
  148. HttpStatus::header( $n );
  149. }
  150. header( "Content-Type: " . $this->mContentType );
  151. if ( $this->mLastModified ) {
  152. header( "Last-Modified: " . $this->mLastModified );
  153. } else {
  154. header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" );
  155. }
  156. if ( $this->mCacheDuration ) {
  157. # If CDN caches are configured, tell them to cache the response,
  158. # and tell the client to always check with the CDN. Otherwise,
  159. # tell the client to use a cached copy, without a way to purge it.
  160. if ( $this->mConfig->get( 'UseCdn' ) ) {
  161. # Expect explicit purge of the proxy cache, but require end user agents
  162. # to revalidate against the proxy on each visit.
  163. header( 'Cache-Control: s-maxage=' . $this->mCacheDuration . ', must-revalidate, max-age=0' );
  164. } else {
  165. # Let the client do the caching. Cache is not purged.
  166. header( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" );
  167. header( "Cache-Control: s-maxage={$this->mCacheDuration}," .
  168. "public,max-age={$this->mCacheDuration}" );
  169. }
  170. } else {
  171. # always expired, always modified
  172. header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past
  173. header( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1
  174. header( "Pragma: no-cache" ); // HTTP/1.0
  175. }
  176. if ( $this->mVary ) {
  177. header( "Vary: " . $this->mVary );
  178. }
  179. }
  180. /**
  181. * checkLastModified tells the client to use the client-cached response if
  182. * possible. If successful, the AjaxResponse is disabled so that
  183. * any future call to AjaxResponse::printText() have no effect.
  184. *
  185. * @param string $timestamp
  186. * @return bool Returns true if the response code was set to 304 Not Modified.
  187. */
  188. function checkLastModified( $timestamp ) {
  189. global $wgCachePages, $wgCacheEpoch, $wgUser;
  190. $fname = 'AjaxResponse::checkLastModified';
  191. if ( !$timestamp || $timestamp == '19700101000000' ) {
  192. wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP", 'private' );
  193. return false;
  194. }
  195. if ( !$wgCachePages ) {
  196. wfDebug( "$fname: CACHE DISABLED", 'private' );
  197. return false;
  198. }
  199. $timestamp = wfTimestamp( TS_MW, $timestamp );
  200. $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->getTouched(), $wgCacheEpoch ) );
  201. if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
  202. # IE sends sizes after the date like this:
  203. # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
  204. # this breaks strtotime().
  205. $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
  206. $modsinceTime = strtotime( $modsince );
  207. $ismodsince = wfTimestamp( TS_MW, $modsinceTime ?: 1 );
  208. wfDebug( "$fname: -- client send If-Modified-Since: $modsince", 'private' );
  209. wfDebug( "$fname: -- we might send Last-Modified : $lastmod", 'private' );
  210. if ( ( $ismodsince >= $timestamp )
  211. && $wgUser->validateCache( $ismodsince ) &&
  212. $ismodsince >= $wgCacheEpoch
  213. ) {
  214. ini_set( 'zlib.output_compression', 0 );
  215. $this->setResponseCode( 304 );
  216. $this->disable();
  217. $this->mLastModified = $lastmod;
  218. wfDebug( "$fname: CACHED client: $ismodsince ; user: {$wgUser->getTouched()} ; " .
  219. "page: $timestamp ; site $wgCacheEpoch", 'private' );
  220. return true;
  221. } else {
  222. wfDebug( "$fname: READY client: $ismodsince ; user: {$wgUser->getTouched()} ; " .
  223. "page: $timestamp ; site $wgCacheEpoch", 'private' );
  224. $this->mLastModified = $lastmod;
  225. }
  226. } else {
  227. wfDebug( "$fname: client did not send If-Modified-Since header", 'private' );
  228. $this->mLastModified = $lastmod;
  229. }
  230. return false;
  231. }
  232. /**
  233. * @param string $mckey
  234. * @param int $touched
  235. * @return bool
  236. */
  237. function loadFromMemcached( $mckey, $touched ) {
  238. if ( !$touched ) {
  239. return false;
  240. }
  241. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  242. $mcvalue = $cache->get( $mckey );
  243. if ( $mcvalue ) {
  244. # Check to see if the value has been invalidated
  245. if ( $touched <= $mcvalue['timestamp'] ) {
  246. wfDebug( "Got $mckey from cache" );
  247. $this->mText = $mcvalue['value'];
  248. return true;
  249. } else {
  250. wfDebug( "$mckey has expired" );
  251. }
  252. }
  253. return false;
  254. }
  255. /**
  256. * @param string $mckey
  257. * @param int $expiry
  258. * @return bool
  259. */
  260. function storeInMemcached( $mckey, $expiry = 86400 ) {
  261. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  262. $cache->set( $mckey,
  263. [
  264. 'timestamp' => wfTimestampNow(),
  265. 'value' => $this->mText
  266. ],
  267. $expiry
  268. );
  269. return true;
  270. }
  271. }