ApiFormatBase.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. /**
  3. *
  4. *
  5. * Created on Sep 19, 2006
  6. *
  7. * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
  8. *
  9. * This program is free software; you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation; either version 2 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License along
  20. * with this program; if not, write to the Free Software Foundation, Inc.,
  21. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. * http://www.gnu.org/copyleft/gpl.html
  23. *
  24. * @file
  25. */
  26. /**
  27. * This is the abstract base class for API formatters.
  28. *
  29. * @ingroup API
  30. */
  31. abstract class ApiFormatBase extends ApiBase {
  32. private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp;
  33. private $mBuffer, $mDisabled = false;
  34. private $mIsWrappedHtml = false;
  35. private $mHttpStatus = false;
  36. protected $mForceDefaultParams = false;
  37. /**
  38. * If $format ends with 'fm', pretty-print the output in HTML.
  39. * @param ApiMain $main
  40. * @param string $format Format name
  41. */
  42. public function __construct( ApiMain $main, $format ) {
  43. parent::__construct( $main, $format );
  44. $this->mIsHtml = ( substr( $format, -2, 2 ) === 'fm' ); // ends with 'fm'
  45. if ( $this->mIsHtml ) {
  46. $this->mFormat = substr( $format, 0, -2 ); // remove ending 'fm'
  47. $this->mIsWrappedHtml = $this->getMain()->getCheck( 'wrappedhtml' );
  48. } else {
  49. $this->mFormat = $format;
  50. }
  51. $this->mFormat = strtoupper( $this->mFormat );
  52. }
  53. /**
  54. * Overriding class returns the MIME type that should be sent to the client.
  55. *
  56. * When getIsHtml() returns true, the return value here is used for syntax
  57. * highlighting but the client sees text/html.
  58. *
  59. * @return string
  60. */
  61. abstract public function getMimeType();
  62. /**
  63. * Return a filename for this module's output.
  64. * @note If $this->getIsWrappedHtml() || $this->getIsHtml(), you'll very
  65. * likely want to fall back to this class's version.
  66. * @since 1.27
  67. * @return string Generally this should be "api-result.$ext", and must be
  68. * encoded for inclusion in a Content-Disposition header's filename parameter.
  69. */
  70. public function getFilename() {
  71. if ( $this->getIsWrappedHtml() ) {
  72. return 'api-result-wrapped.json';
  73. } elseif ( $this->getIsHtml() ) {
  74. return 'api-result.html';
  75. } else {
  76. $exts = MimeMagic::singleton()->getExtensionsForType( $this->getMimeType() );
  77. $ext = $exts ? strtok( $exts, ' ' ) : strtolower( $this->mFormat );
  78. return "api-result.$ext";
  79. }
  80. }
  81. /**
  82. * Get the internal format name
  83. * @return string
  84. */
  85. public function getFormat() {
  86. return $this->mFormat;
  87. }
  88. /**
  89. * Returns true when the HTML pretty-printer should be used.
  90. * The default implementation assumes that formats ending with 'fm'
  91. * should be formatted in HTML.
  92. * @return bool
  93. */
  94. public function getIsHtml() {
  95. return $this->mIsHtml;
  96. }
  97. /**
  98. * Returns true when the special wrapped mode is enabled.
  99. * @since 1.27
  100. * @return bool
  101. */
  102. protected function getIsWrappedHtml() {
  103. return $this->mIsWrappedHtml;
  104. }
  105. /**
  106. * Disable the formatter.
  107. *
  108. * This causes calls to initPrinter() and closePrinter() to be ignored.
  109. */
  110. public function disable() {
  111. $this->mDisabled = true;
  112. }
  113. /**
  114. * Whether the printer is disabled
  115. * @return bool
  116. */
  117. public function isDisabled() {
  118. return $this->mDisabled;
  119. }
  120. /**
  121. * Whether this formatter can handle printing API errors.
  122. *
  123. * If this returns false, then on API errors the default printer will be
  124. * instantiated.
  125. * @since 1.23
  126. * @return bool
  127. */
  128. public function canPrintErrors() {
  129. return true;
  130. }
  131. /**
  132. * Ignore request parameters, force a default.
  133. *
  134. * Used as a fallback if errors are being thrown.
  135. * @since 1.26
  136. */
  137. public function forceDefaultParams() {
  138. $this->mForceDefaultParams = true;
  139. }
  140. /**
  141. * Overridden to honor $this->forceDefaultParams(), if applicable
  142. * @inheritDoc
  143. * @since 1.26
  144. */
  145. protected function getParameterFromSettings( $paramName, $paramSettings, $parseLimit ) {
  146. if ( !$this->mForceDefaultParams ) {
  147. return parent::getParameterFromSettings( $paramName, $paramSettings, $parseLimit );
  148. }
  149. if ( !is_array( $paramSettings ) ) {
  150. return $paramSettings;
  151. } elseif ( isset( $paramSettings[self::PARAM_DFLT] ) ) {
  152. return $paramSettings[self::PARAM_DFLT];
  153. } else {
  154. return null;
  155. }
  156. }
  157. /**
  158. * Set the HTTP status code to be used for the response
  159. * @since 1.29
  160. * @param int $code
  161. */
  162. public function setHttpStatus( $code ) {
  163. if ( $this->mDisabled ) {
  164. return;
  165. }
  166. if ( $this->getIsHtml() ) {
  167. $this->mHttpStatus = $code;
  168. } else {
  169. $this->getMain()->getRequest()->response()->statusHeader( $code );
  170. }
  171. }
  172. /**
  173. * Initialize the printer function and prepare the output headers.
  174. * @param bool $unused Always false since 1.25
  175. */
  176. public function initPrinter( $unused = false ) {
  177. if ( $this->mDisabled ) {
  178. return;
  179. }
  180. $mime = $this->getIsWrappedHtml()
  181. ? 'text/mediawiki-api-prettyprint-wrapped'
  182. : ( $this->getIsHtml() ? 'text/html' : $this->getMimeType() );
  183. // Some printers (ex. Feed) do their own header settings,
  184. // in which case $mime will be set to null
  185. if ( $mime === null ) {
  186. return; // skip any initialization
  187. }
  188. $this->getMain()->getRequest()->response()->header( "Content-Type: $mime; charset=utf-8" );
  189. // Set X-Frame-Options API results (T41180)
  190. $apiFrameOptions = $this->getConfig()->get( 'ApiFrameOptions' );
  191. if ( $apiFrameOptions ) {
  192. $this->getMain()->getRequest()->response()->header( "X-Frame-Options: $apiFrameOptions" );
  193. }
  194. // Set a Content-Disposition header so something downloading an API
  195. // response uses a halfway-sensible filename (T128209).
  196. $filename = $this->getFilename();
  197. $this->getMain()->getRequest()->response()->header(
  198. "Content-Disposition: inline; filename=\"{$filename}\""
  199. );
  200. }
  201. /**
  202. * Finish printing and output buffered data.
  203. */
  204. public function closePrinter() {
  205. if ( $this->mDisabled ) {
  206. return;
  207. }
  208. $mime = $this->getMimeType();
  209. if ( $this->getIsHtml() && $mime !== null ) {
  210. $format = $this->getFormat();
  211. $lcformat = strtolower( $format );
  212. $result = $this->getBuffer();
  213. $context = new DerivativeContext( $this->getMain() );
  214. $context->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'apioutput' ) );
  215. $context->setTitle( SpecialPage::getTitleFor( 'ApiHelp' ) );
  216. $out = new OutputPage( $context );
  217. $context->setOutput( $out );
  218. $out->addModuleStyles( 'mediawiki.apipretty' );
  219. $out->setPageTitle( $context->msg( 'api-format-title' ) );
  220. if ( !$this->getIsWrappedHtml() ) {
  221. // When the format without suffix 'fm' is defined, there is a non-html version
  222. if ( $this->getMain()->getModuleManager()->isDefined( $lcformat, 'format' ) ) {
  223. if ( !$this->getRequest()->wasPosted() ) {
  224. $nonHtmlUrl = strtok( $this->getRequest()->getFullRequestURL(), '?' )
  225. . '?' . $this->getRequest()->appendQueryValue( 'format', $lcformat );
  226. $msg = $context->msg( 'api-format-prettyprint-header-hyperlinked' )
  227. ->params( $format, $lcformat, $nonHtmlUrl );
  228. } else {
  229. $msg = $context->msg( 'api-format-prettyprint-header' )->params( $format, $lcformat );
  230. }
  231. } else {
  232. $msg = $context->msg( 'api-format-prettyprint-header-only-html' )->params( $format );
  233. }
  234. $header = $msg->parseAsBlock();
  235. $out->addHTML(
  236. Html::rawElement( 'div', [ 'class' => 'api-pretty-header' ],
  237. ApiHelp::fixHelpLinks( $header )
  238. )
  239. );
  240. if ( $this->mHttpStatus && $this->mHttpStatus !== 200 ) {
  241. $out->addHTML(
  242. Html::rawElement( 'div', [ 'class' => 'api-pretty-header api-pretty-status' ],
  243. $this->msg(
  244. 'api-format-prettyprint-status',
  245. $this->mHttpStatus,
  246. HttpStatus::getMessage( $this->mHttpStatus )
  247. )->parse()
  248. )
  249. );
  250. }
  251. }
  252. if ( Hooks::run( 'ApiFormatHighlight', [ $context, $result, $mime, $format ] ) ) {
  253. $out->addHTML(
  254. Html::element( 'pre', [ 'class' => 'api-pretty-content' ], $result )
  255. );
  256. }
  257. if ( $this->getIsWrappedHtml() ) {
  258. // This is a special output mode mainly intended for ApiSandbox use
  259. $time = microtime( true ) - $this->getConfig()->get( 'RequestTime' );
  260. $json = FormatJson::encode(
  261. [
  262. 'status' => (int)( $this->mHttpStatus ?: 200 ),
  263. 'statustext' => HttpStatus::getMessage( $this->mHttpStatus ?: 200 ),
  264. 'html' => $out->getHTML(),
  265. 'modules' => array_values( array_unique( array_merge(
  266. $out->getModules(),
  267. $out->getModuleScripts(),
  268. $out->getModuleStyles()
  269. ) ) ),
  270. 'continue' => $this->getResult()->getResultData( 'continue' ),
  271. 'time' => round( $time * 1000 ),
  272. ],
  273. false, FormatJson::ALL_OK
  274. );
  275. // T68776: wfMangleFlashPolicy() is needed to avoid a nasty bug in
  276. // Flash, but what it does isn't friendly for the API, so we need to
  277. // work around it.
  278. if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $json ) ) {
  279. $json = preg_replace(
  280. '/\<(\s*cross-domain-policy\s*)\>/i', '\\u003C$1\\u003E', $json
  281. );
  282. }
  283. echo $json;
  284. } else {
  285. // API handles its own clickjacking protection.
  286. // Note, that $wgBreakFrames will still override $wgApiFrameOptions for format mode.
  287. $out->allowClickjacking();
  288. $out->output();
  289. }
  290. } else {
  291. // For non-HTML output, clear all errors that might have been
  292. // displayed if display_errors=On
  293. ob_clean();
  294. echo $this->getBuffer();
  295. }
  296. }
  297. /**
  298. * Append text to the output buffer.
  299. * @param string $text
  300. */
  301. public function printText( $text ) {
  302. $this->mBuffer .= $text;
  303. }
  304. /**
  305. * Get the contents of the buffer.
  306. * @return string
  307. */
  308. public function getBuffer() {
  309. return $this->mBuffer;
  310. }
  311. public function getAllowedParams() {
  312. $ret = [];
  313. if ( $this->getIsHtml() ) {
  314. $ret['wrappedhtml'] = [
  315. ApiBase::PARAM_DFLT => false,
  316. ApiBase::PARAM_HELP_MSG => 'apihelp-format-param-wrappedhtml',
  317. ];
  318. }
  319. return $ret;
  320. }
  321. protected function getExamplesMessages() {
  322. return [
  323. 'action=query&meta=siteinfo&siprop=namespaces&format=' . $this->getModuleName()
  324. => [ 'apihelp-format-example-generic', $this->getFormat() ]
  325. ];
  326. }
  327. public function getHelpUrls() {
  328. return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats';
  329. }
  330. }
  331. /**
  332. * For really cool vim folding this needs to be at the end:
  333. * vim: foldmarker=@{,@} foldmethod=marker
  334. */