PageRenderTime 64ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 1ms

/extensions/Translate/tag/TranslatablePage.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 749 lines | 432 code | 142 blank | 175 comment | 40 complexity | 04b6aa28c36d16ac4e077ef1f99b1775 MD5 | raw file
  1. <?php
  2. /**
  3. * Translatable page model.
  4. *
  5. * @file
  6. * @author Niklas Laxström
  7. * @copyright Copyright © 2009-2011 Niklas Laxström
  8. * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
  9. */
  10. /**
  11. * Class to parse translatable wiki pages.
  12. * @ingroup PageTranslation
  13. */
  14. class TranslatablePage {
  15. /**
  16. * Title of the page.
  17. */
  18. protected $title = null;
  19. /**
  20. * Text contents of the page.
  21. */
  22. protected $text = null;
  23. /**
  24. * Revision of the page, if applicaple.
  25. */
  26. protected $revision = null;
  27. /**
  28. * From which source this object was constructed.
  29. * Can be: text, revision, title
  30. */
  31. protected $source = null;
  32. /**
  33. * Whether the page contents is already loaded.
  34. */
  35. protected $init = false;
  36. /**
  37. * Name of the section which contains the translated page title.
  38. */
  39. protected $displayTitle = 'Page display title';
  40. /**
  41. * @param title Title object for the page
  42. */
  43. protected function __construct( Title $title ) {
  44. $this->title = $title;
  45. }
  46. // Public constructors //
  47. /**
  48. * Constructs a translatable page from given text.
  49. * Some functions will fail unless you set revision
  50. * parameter manually.
  51. */
  52. public static function newFromText( Title $title, $text ) {
  53. $obj = new self( $title );
  54. $obj->text = $text;
  55. $obj->source = 'text';
  56. return $obj;
  57. }
  58. /**
  59. * Constructs a translatable page from given revision.
  60. * The revision must belong to the title given or unspecified
  61. * behaviour will happen.
  62. *
  63. * @param $title Title
  64. * @param $revision integer Revision number
  65. * @return TranslatablePage
  66. */
  67. public static function newFromRevision( Title $title, $revision ) {
  68. $rev = Revision::newFromTitle( $title, $revision );
  69. if ( $rev === null ) {
  70. throw new MWException( 'Revision is null' );
  71. }
  72. $obj = new self( $title );
  73. $obj->source = 'revision';
  74. $obj->revision = $revision;
  75. return $obj;
  76. }
  77. /**
  78. * Constructs a translatable page from title.
  79. * The text of last marked revision is loaded when neded.
  80. *
  81. * @param $title Title
  82. * @return TranslatablePage
  83. */
  84. public static function newFromTitle( Title $title ) {
  85. $obj = new self( $title );
  86. $obj->source = 'title';
  87. return $obj;
  88. }
  89. // Getters //
  90. /**
  91. * Returns the title for this translatable page.
  92. * @return Title
  93. */
  94. public function getTitle() {
  95. return $this->title;
  96. }
  97. /**
  98. * Returns the text for this translatable page.
  99. * @return \string
  100. */
  101. public function getText() {
  102. if ( $this->init === false ) {
  103. switch ( $this->source ) {
  104. case 'text':
  105. break;
  106. case 'title':
  107. $this->revision = $this->getMarkedTag();
  108. case 'revision':
  109. $rev = Revision::newFromTitle( $this->getTitle(), $this->revision );
  110. $this->text = $rev->getText();
  111. break;
  112. }
  113. }
  114. if ( !is_string( $this->text ) ) {
  115. throw new MWException( 'We have no text' );
  116. }
  117. $this->init = true;
  118. return $this->text;
  119. }
  120. /**
  121. * Revision is null if object was constructed using newFromText.
  122. * @return null or integer
  123. */
  124. public function getRevision() {
  125. return $this->revision;
  126. }
  127. /**
  128. * Manually set a revision number to use loading page text.
  129. * @param $revision integer
  130. */
  131. public function setRevision( $revision ) {
  132. $this->revision = $revision;
  133. $this->source = 'revision';
  134. $this->init = false;
  135. }
  136. // Public functions //
  137. /**
  138. * Returns MessageGroup id (to be) used for translating this page.
  139. * @return \string
  140. */
  141. public function getMessageGroupId() {
  142. return self::getMessageGroupIdFromTitle( $this->getTitle() );
  143. }
  144. /**
  145. * Constructs MessageGroup id for any title.
  146. * @param $title Title
  147. * @return \string
  148. */
  149. public static function getMessageGroupIdFromTitle( Title $title ) {
  150. return 'page|' . $title->getPrefixedText();
  151. }
  152. /**
  153. * Returns MessageGroup used for translating this page. It may still be empty
  154. * if the page has not been ever marked.
  155. * @return \type{WikiPageMessageGroup}
  156. */
  157. public function getMessageGroup() {
  158. return MessageGroups::getGroup( $this->getMessageGroupId() );
  159. }
  160. /**
  161. * Get translated page title.
  162. * @param $code \string Language code.
  163. * @return \string or null
  164. */
  165. public function getPageDisplayTitle( $code ) {
  166. $section = str_replace( ' ', '_', $this->displayTitle );
  167. $page = $this->getTitle()->getPrefixedDBKey();
  168. return $this->getMessageGroup()->getMessage( "$page/$section", $code );
  169. }
  170. /**
  171. * Returns a TPParse object which represents the parsed page.
  172. * @throws TPExcetion if the page is malformed as a translatable
  173. * page.
  174. * @return TPParse
  175. */
  176. public function getParse() {
  177. if ( isset( $this->cachedParse ) ) {
  178. return $this->cachedParse;
  179. }
  180. $text = $this->getText();
  181. $nowiki = array();
  182. $text = self::armourNowiki( $nowiki, $text );
  183. $sections = array();
  184. // Add section to allow translating the page name
  185. $displaytitle = new TPSection;
  186. $displaytitle->id = $this->displayTitle;
  187. $displaytitle->text = $this->getTitle()->getPrefixedText();
  188. $sections[self::getUniq()] = $displaytitle;
  189. $tagPlaceHolders = array();
  190. while ( true ) {
  191. $re = '~(<translate>)\s*(.*?)(</translate>)~s';
  192. $matches = array();
  193. $ok = preg_match_all( $re, $text, $matches, PREG_OFFSET_CAPTURE );
  194. if ( $ok === 0 ) {
  195. break; // No matches
  196. }
  197. // Do-placehold for the whole stuff
  198. $ph = self::getUniq();
  199. $start = $matches[0][0][1];
  200. $len = strlen( $matches[0][0][0] );
  201. $end = $start + $len;
  202. $text = self::index_replace( $text, $ph, $start, $end );
  203. // Sectionise the contents
  204. // Strip the surrounding tags
  205. $contents = $matches[0][0][0]; // full match
  206. $start = $matches[2][0][1] - $matches[0][0][1]; // bytes before actual content
  207. $len = strlen( $matches[2][0][0] ); // len of the content
  208. $end = $start + $len;
  209. $sectiontext = substr( $contents, $start, $len );
  210. if ( strpos( $sectiontext, '<translate>' ) !== false ) {
  211. throw new TPException( array( 'pt-parse-nested', $sectiontext ) );
  212. }
  213. $sectiontext = self::unArmourNowiki( $nowiki, $sectiontext );
  214. $ret = $this->sectionise( $sections, $sectiontext );
  215. $tagPlaceHolders[$ph] =
  216. self::index_replace( $contents, $ret, $start, $end );
  217. }
  218. $prettyTemplate = $text;
  219. foreach ( $tagPlaceHolders as $ph => $value ) {
  220. $prettyTemplate = str_replace( $ph, '[...]', $prettyTemplate );
  221. }
  222. if ( strpos( $text, '<translate>' ) !== false ) {
  223. throw new TPException( array( 'pt-parse-open', $prettyTemplate ) );
  224. } elseif ( strpos( $text, '</translate>' ) !== false ) {
  225. throw new TPException( array( 'pt-parse-close', $prettyTemplate ) );
  226. }
  227. foreach ( $tagPlaceHolders as $ph => $value ) {
  228. $text = str_replace( $ph, $value, $text );
  229. }
  230. if ( count( $sections ) === 1 ) {
  231. // Don't return display title for pages which have no sections
  232. $sections = array();
  233. }
  234. $text = self::unArmourNowiki( $nowiki, $text );
  235. $parse = new TPParse( $this->getTitle() );
  236. $parse->template = $text;
  237. $parse->sections = $sections;
  238. // Cache it
  239. $this->cachedParse = $parse;
  240. return $parse;
  241. }
  242. // Inner functionality //
  243. public static function armourNowiki( &$holders, $text ) {
  244. $re = '~(<nowiki>)(.*?)(</nowiki>)~s';
  245. while ( preg_match( $re, $text, $matches ) ) {
  246. $ph = self::getUniq();
  247. $text = str_replace( $matches[0], $ph, $text );
  248. $holders[$ph] = $matches[0];
  249. }
  250. return $text;
  251. }
  252. public static function unArmourNowiki( $holders, $text ) {
  253. foreach ( $holders as $ph => $value ) {
  254. $text = str_replace( $ph, $value, $text );
  255. }
  256. return $text;
  257. }
  258. /**
  259. * Returns a random string that can be used as placeholder.
  260. */
  261. protected static function getUniq() {
  262. static $i = 0;
  263. return "\x7fUNIQ" . dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) ) . '|' . $i++;
  264. }
  265. protected static function index_replace( $string, $rep, $start, $end ) {
  266. return substr( $string, 0, $start ) . $rep . substr( $string, $end );
  267. }
  268. /**
  269. * Splits the content marked with \<translate> tags into sections, which
  270. * are separated with with two or more newlines. Extra whitespace is captured
  271. * in the template and not included in the sections.
  272. * @param $sections Array of placeholder => TPSection.
  273. * @param $text Contents of one pair of \<translate> tags.
  274. * @return \string Templace with placeholders for sections, which itself are added to $sections.
  275. */
  276. protected function sectionise( &$sections, $text ) {
  277. $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE;
  278. $parts = preg_split( '~(\s*\n\n\s*|\s*$)~', $text, -1, $flags );
  279. $template = '';
  280. foreach ( $parts as $_ ) {
  281. if ( trim( $_ ) === '' ) {
  282. $template .= $_;
  283. } else {
  284. $ph = self::getUniq();
  285. $sections[$ph] = $this->shakeSection( $_ );
  286. $template .= $ph;
  287. }
  288. }
  289. return $template;
  290. }
  291. /**
  292. * Checks if this section already contains a section marker. If there
  293. * is not, a new one will be created. Marker will have the value of
  294. * -1, which will later be replaced with a real value.
  295. *
  296. * May throw a TPException if there is error with existing section
  297. * markers.
  298. *
  299. * @param $content string Content of one section
  300. * @return TPSection
  301. */
  302. protected function shakeSection( $content ) {
  303. $re = '~<!--T:(.*?)-->~';
  304. $matches = array();
  305. $count = preg_match_all( $re, $content, $matches, PREG_SET_ORDER );
  306. if ( $count > 1 ) {
  307. throw new TPException( array( 'pt-shake-multiple', $content ) );
  308. }
  309. $section = new TPSection;
  310. if ( $count === 1 ) {
  311. foreach ( $matches as $match ) {
  312. list( /*full*/, $id ) = $match;
  313. $section->id = $id;
  314. // Currently handle only these two standard places.
  315. // Is this too strict?
  316. $rer1 = '~^<!--T:(.*?)-->\n~'; // Normal sections
  317. $rer2 = '~\s*<!--T:(.*?)-->$~m'; // Sections with title
  318. $content = preg_replace( $rer1, '', $content );
  319. $content = preg_replace( $rer2, '', $content );
  320. if ( preg_match( $re, $content ) === 1 ) {
  321. throw new TPException( array( 'pt-shake-position', $content ) );
  322. } elseif ( trim( $content ) === '' ) {
  323. throw new TPException( array( 'pt-shake-empty', $id ) );
  324. }
  325. }
  326. } else {
  327. // New section
  328. $section->id = -1;
  329. }
  330. $section->text = $content;
  331. return $section;
  332. }
  333. // Tag methods //
  334. protected static $tagCache = array();
  335. /**
  336. * Adds a tag which indicates that this page is
  337. * suitable for translation.
  338. * @param $revision integer
  339. */
  340. public function addMarkedTag( $revision, $value = null ) {
  341. $this->addTag( 'tp:mark', $revision, $value );
  342. MessageGroups::clearCache();
  343. }
  344. /**
  345. * Adds a tag which indicates that this page source is
  346. * ready for marking for translation.
  347. * @param $revision integer
  348. */
  349. public function addReadyTag( $revision ) {
  350. $this->addTag( 'tp:tag', $revision );
  351. }
  352. protected function addTag( $tag, $revision, $value = null ) {
  353. $dbw = wfGetDB( DB_MASTER );
  354. $aid = $this->getTitle()->getArticleId();
  355. if ( is_object( $revision ) ) {
  356. throw new MWException( 'Got object, excepted id' );
  357. }
  358. $conds = array(
  359. 'rt_page' => $aid,
  360. 'rt_type' => RevTag::getType( $tag ),
  361. 'rt_revision' => $revision
  362. );
  363. $dbw->delete( 'revtag', $conds, __METHOD__ );
  364. if ( $value !== null ) {
  365. $conds['rt_value'] = serialize( implode( '|', $value ) );
  366. }
  367. $dbw->insert( 'revtag', $conds, __METHOD__ );
  368. self::$tagCache[$aid][$tag] = $revision;
  369. }
  370. /**
  371. * Returns the latest revision which has marked tag, if any.
  372. * @param $db Database connection type
  373. * @return integer|false
  374. */
  375. public function getMarkedTag( $db = DB_SLAVE ) {
  376. return $this->getTag( 'tp:mark' );
  377. }
  378. /**
  379. * Returns the latest revision which has ready tag, if any.
  380. * @param $db Database connection type
  381. * @return integer|false
  382. */
  383. public function getReadyTag( $db = DB_SLAVE ) {
  384. return $this->getTag( 'tp:tag' );
  385. }
  386. /**
  387. * Removes all page translation feature data from the database.
  388. * Does not remove translated sections or translation pages.
  389. * @todo Change name to something better.
  390. */
  391. public function removeTags() {
  392. $aid = $this->getTitle()->getArticleId();
  393. $dbw = wfGetDB( DB_MASTER );
  394. $conds = array(
  395. 'rt_page' => $aid,
  396. 'rt_type' => array(
  397. RevTag::getType( 'tp:mark' ),
  398. RevTag::getType( 'tp:tag' ),
  399. ),
  400. );
  401. $dbw->delete( 'revtag', $conds, __METHOD__ );
  402. $dbw->delete( 'translate_sections', array( 'trs_page' => $aid ), __METHOD__ );
  403. unset( self::$tagCache[$aid] );
  404. }
  405. /// @return false if tag is not found
  406. protected function getTag( $tag, $dbt = DB_SLAVE ) {
  407. if ( !$this->getTitle()->exists() ) {
  408. return false;
  409. }
  410. $aid = $this->getTitle()->getArticleId();
  411. if ( isset( self::$tagCache[$aid][$tag] ) ) {
  412. return self::$tagCache[$aid][$tag];
  413. }
  414. $db = wfGetDB( $dbt );
  415. $conds = array(
  416. 'rt_page' => $aid,
  417. 'rt_type' => RevTag::getType( $tag ),
  418. );
  419. $options = array( 'ORDER BY' => 'rt_revision DESC' );
  420. // Tag values are not stored, only the associated revision
  421. $tagRevision = $db->selectField( 'revtag', 'rt_revision', $conds, __METHOD__, $options );
  422. if ( $tagRevision !== false ) {
  423. return self::$tagCache[$aid][$tag] = intval( $tagRevision );
  424. } else {
  425. return self::$tagCache[$aid][$tag] = false;
  426. }
  427. }
  428. public function getTranslationUrl( $code = false ) {
  429. $translate = SpecialPage::getTitleFor( 'Translate' );
  430. $params = array(
  431. 'group' => $this->getMessageGroupId(),
  432. 'task' => 'view',
  433. 'language' => $code,
  434. );
  435. return $translate->getFullURL( $params );
  436. }
  437. public function getMarkedRevs() {
  438. $db = wfGetDB( DB_SLAVE );
  439. $fields = array( 'rt_revision', 'rt_value' );
  440. $conds = array(
  441. 'rt_page' => $this->getTitle()->getArticleId(),
  442. 'rt_type' => RevTag::getType( 'tp:mark' ),
  443. );
  444. $options = array( 'ORDER BY' => 'rt_revision DESC' );
  445. return $db->select( 'revtag', $fields, $conds, __METHOD__, $options );
  446. }
  447. public function getTranslationPages() {
  448. // Fetch the available translation pages from database
  449. $dbr = wfGetDB( DB_SLAVE );
  450. $prefix = $this->getTitle()->getDBkey() . '/';
  451. $likePattern = $dbr->buildLike( $prefix, $dbr->anyString() );
  452. $res = $dbr->select(
  453. 'page',
  454. array( 'page_namespace', 'page_title' ),
  455. array(
  456. 'page_namespace' => $this->getTitle()->getNamespace(),
  457. "page_title $likePattern"
  458. ),
  459. __METHOD__
  460. );
  461. $titles = TitleArray::newFromResult( $res );
  462. $filtered = array();
  463. // Make sure we only get translation subpages while ignoring others
  464. $codes = Language::getLanguageNames( false );
  465. $prefix = $this->getTitle()->getText();
  466. foreach ( $titles as $title ) {
  467. list( $name, $code ) = TranslateUtils::figureMessage( $title->getText() );
  468. if ( !isset( $codes[$code] ) || $name !== $prefix ) {
  469. continue;
  470. }
  471. $filtered[] = $title;
  472. }
  473. return $filtered;
  474. }
  475. public function getTranslationPercentages( $force = false ) {
  476. // Check the memory cache, as this is very slow to calculate
  477. global $wgMemc, $wgRequest;
  478. $memcKey = wfMemcKey( 'pt', 'status', $this->getTitle()->getPrefixedText() );
  479. $cache = $wgMemc->get( $memcKey );
  480. $force = $force || $wgRequest->getText( 'action' ) === 'purge';
  481. if ( !$force && is_array( $cache ) ) {
  482. return $cache;
  483. }
  484. $titles = $this->getTranslationPages();
  485. // Calculate percentages for the available translations
  486. $group = $this->getMessageGroup();
  487. if ( !$group instanceof WikiPageMessageGroup ) {
  488. return null;
  489. }
  490. $markedRevs = $this->getMarkedRevs();
  491. $temp = array();
  492. foreach ( $titles as $t ) {
  493. list( , $code ) = TranslateUtils::figureMessage( $t->getText() );
  494. $collection = $group->initCollection( $code );
  495. $percent = $this->getPercentageInternal( $collection, $markedRevs );
  496. // To avoid storing 40 decimals of inaccuracy, truncate to two decimals
  497. $temp[$collection->code] = sprintf( '%.2f', $percent );
  498. }
  499. // Content language is always up-to-date
  500. global $wgContLang;
  501. $temp[$wgContLang->getCode()] = 1.00;
  502. $wgMemc->set( $memcKey, $temp, 60 * 60 * 12 );
  503. return $temp;
  504. }
  505. protected function getPercentageInternal( $collection, $markedRevs ) {
  506. $count = count( $collection );
  507. if ( $count === 0 ) {
  508. return 0;
  509. }
  510. // We want to get fuzzy though
  511. $collection->filter( 'hastranslation', false );
  512. $collection->initMessages();
  513. $total = 0;
  514. foreach ( $collection as $key => $message ) {
  515. $score = 1;
  516. // Fuzzy halves score
  517. if ( $message->hasTag( 'fuzzy' ) ) {
  518. $score *= 0.5;
  519. /* Reduce 20% for every newer revision than what is translated against.
  520. * This is inside fuzzy clause, because there might be silent changes
  521. * which we don't want to decrease the translation percentage.
  522. */
  523. $rev = $this->getTransrev( $key . '/' . $collection->code );
  524. foreach ( $markedRevs as $r ) {
  525. if ( $rev === $r->rt_revision ) break;
  526. $changed = explode( '|', unserialize( $r->rt_value ) );
  527. // Get a suitable section key
  528. $parts = explode( '/', $key );
  529. $ikey = $parts[count( $parts ) - 1];
  530. // If the section was changed, reduce the score
  531. if ( in_array( $ikey, $changed, true ) ) {
  532. $score *= 0.8;
  533. }
  534. }
  535. }
  536. $total += $score;
  537. }
  538. // Divide score by count to get completion percentage
  539. return $total / $count;
  540. }
  541. public function getTransRev( $suffix ) {
  542. $title = Title::makeTitle( NS_TRANSLATIONS, $suffix );
  543. $db = wfGetDB( DB_SLAVE );
  544. $fields = 'rt_value';
  545. $conds = array(
  546. 'rt_page' => $title->getArticleId(),
  547. 'rt_type' => RevTag::getType( 'tp:transver' ),
  548. );
  549. $options = array( 'ORDER BY' => 'rt_revision DESC' );
  550. return $db->selectField( 'revtag', $fields, $conds, __METHOD__, $options );
  551. }
  552. public static function isTranslationPage( Title $title ) {
  553. list( $key, $code ) = TranslateUtils::figureMessage( $title->getText() );
  554. if ( $key === '' || $code === '' ) {
  555. return false;
  556. }
  557. $codes = Language::getLanguageNames( false );
  558. global $wgTranslateDocumentationLanguageCode;
  559. unset( $codes[$wgTranslateDocumentationLanguageCode] );
  560. if ( !isset( $codes[$code] ) ) {
  561. return false;
  562. }
  563. $newtitle = self::changeTitleText( $title, $key );
  564. if ( !$newtitle ) {
  565. return false;
  566. }
  567. $page = TranslatablePage::newFromTitle( $newtitle );
  568. if ( $page->getMarkedTag() === false ) {
  569. return false;
  570. }
  571. return $page;
  572. }
  573. protected static function changeTitleText( Title $title, $text ) {
  574. return Title::makeTitleSafe( $title->getNamespace(), $text );
  575. }
  576. public static function isSourcePage( Title $title ) {
  577. static $cache = null;
  578. $cacheObj = wfGetCache( CACHE_ANYTHING );
  579. $cacheKey = wfMemcKey( 'pagetranslation', 'sourcepages' );
  580. if ( $cache === null ) {
  581. $cache = $cacheObj->get( $cacheKey );
  582. }
  583. if ( !is_array( $cache ) ) {
  584. $cache = self::getTranslatablePages();
  585. $cacheObj->set( $cacheKey, $cache, 60 * 5 );
  586. }
  587. return in_array( $title->getArticleId(), $cache );
  588. }
  589. /// List of page ids where the latest revision is either tagged or marked
  590. public static function getTranslatablePages() {
  591. $dbr = wfGetDB( DB_SLAVE );
  592. $tables = array( 'revtag', 'page' );
  593. $fields = 'rt_page';
  594. $conds = array(
  595. 'rt_page = page_id',
  596. 'rt_revision = page_latest',
  597. 'rt_type' => array( RevTag::getType( 'tp:mark' ), RevTag::getType( 'tp:tag' ) ),
  598. );
  599. $options = array( 'GROUP BY' => 'rt_page' );
  600. $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options );
  601. $results = array();
  602. foreach ( $res as $row ) {
  603. $results[] = $row->rt_page;
  604. }
  605. return $results;
  606. }
  607. }