TranslatablePage.php

Go to the documentation of this file.
00001 <?php
00015 class TranslatablePage {
00019     protected $title = null;
00020 
00024     protected $text = null;
00025 
00031     protected $revision = null;
00032 
00037     protected $source = null;
00038 
00042     protected $init = false;
00043 
00047     protected $displayTitle = 'Page display title';
00048 
00049     protected $cachedParse;
00050 
00054     protected function __construct( Title $title ) {
00055         $this->title = $title;
00056     }
00057 
00058     // Public constructors //
00059 
00070     public static function newFromText( Title $title, $text ) {
00071         $obj = new self( $title );
00072         $obj->text = $text;
00073         $obj->source = 'text';
00074 
00075         return $obj;
00076     }
00077 
00088     public static function newFromRevision( Title $title, $revision ) {
00089         $rev = Revision::newFromTitle( $title, $revision );
00090         if ( $rev === null ) {
00091             throw new MWException( 'Revision is null' );
00092         }
00093 
00094         $obj = new self( $title );
00095         $obj->source = 'revision';
00096         $obj->revision = $revision;
00097 
00098         return $obj;
00099     }
00100 
00108     public static function newFromTitle( Title $title ) {
00109         $obj = new self( $title );
00110         $obj->source = 'title';
00111 
00112         return $obj;
00113     }
00114 
00115     // Getters //
00116 
00121     public function getTitle() {
00122         return $this->title;
00123     }
00124 
00130     public function getText() {
00131         if ( $this->init === false ) {
00132             switch ( $this->source ) {
00133                 case 'text':
00134                     break;
00135                 case 'title':
00136                     $this->revision = $this->getMarkedTag();
00137                 // There is no break statement here on purpose
00138                 case 'revision':
00139                     $rev = Revision::newFromTitle( $this->getTitle(), $this->revision );
00140                     $this->text = $rev->getText();
00141                     break;
00142             }
00143         }
00144 
00145         if ( !is_string( $this->text ) ) {
00146             throw new MWException( 'We have no text' );
00147         }
00148 
00149         $this->init = true;
00150 
00151         return $this->text;
00152     }
00153 
00158     public function getRevision() {
00159         return $this->revision;
00160     }
00161 
00166     public function setRevision( $revision ) {
00167         $this->revision = $revision;
00168         $this->source = 'revision';
00169         $this->init = false;
00170     }
00171 
00172     // Public functions //
00173 
00180     public function getSourceLanguageCode() {
00181         return $this->getTitle()->getPageLanguage()->getCode();
00182     }
00183 
00188     public function getMessageGroupId() {
00189         return self::getMessageGroupIdFromTitle( $this->getTitle() );
00190     }
00191 
00197     public static function getMessageGroupIdFromTitle( Title $title ) {
00198         return 'page-' . $title->getPrefixedText();
00199     }
00200 
00206     public function getMessageGroup() {
00207         return MessageGroups::getGroup( $this->getMessageGroupId() );
00208     }
00209 
00215     public function getPageDisplayTitle( $code ) {
00216         $section = str_replace( ' ', '_', $this->displayTitle );
00217         $page = $this->getTitle()->getPrefixedDBKey();
00218 
00219         return $this->getMessageGroup()->getMessage( "$page/$section", $code );
00220     }
00221 
00228     public function getParse() {
00229         if ( isset( $this->cachedParse ) ) {
00230             return $this->cachedParse;
00231         }
00232 
00233         $text = $this->getText();
00234 
00235         $nowiki = array();
00236         $text = self::armourNowiki( $nowiki, $text );
00237 
00238         $sections = array();
00239 
00240         // Add section to allow translating the page name
00241         $displaytitle = new TPSection;
00242         $displaytitle->id = $this->displayTitle;
00243         $displaytitle->text = $this->getTitle()->getPrefixedText();
00244         $sections[TranslateUtils::getPlaceholder()] = $displaytitle;
00245 
00246         $tagPlaceHolders = array();
00247 
00248         while ( true ) {
00249             $re = '~(<translate>)\s*(.*?)(</translate>)~s';
00250             $matches = array();
00251             $ok = preg_match_all( $re, $text, $matches, PREG_OFFSET_CAPTURE );
00252 
00253             if ( $ok === 0 ) {
00254                 break; // No matches
00255             }
00256 
00257             // Do-placehold for the whole stuff
00258             $ph = TranslateUtils::getPlaceholder();
00259             $start = $matches[0][0][1];
00260             $len = strlen( $matches[0][0][0] );
00261             $end = $start + $len;
00262             $text = self::index_replace( $text, $ph, $start, $end );
00263 
00264             // Sectionise the contents
00265             // Strip the surrounding tags
00266             $contents = $matches[0][0][0]; // full match
00267             $start = $matches[2][0][1] - $matches[0][0][1]; // bytes before actual content
00268             $len = strlen( $matches[2][0][0] ); // len of the content
00269             $end = $start + $len;
00270 
00271             $sectiontext = substr( $contents, $start, $len );
00272 
00273             if ( strpos( $sectiontext, '<translate>' ) !== false ) {
00274                 throw new TPException( array( 'pt-parse-nested', $sectiontext ) );
00275             }
00276 
00277             $sectiontext = self::unArmourNowiki( $nowiki, $sectiontext );
00278 
00279             $ret = $this->sectionise( $sections, $sectiontext );
00280 
00281             $tagPlaceHolders[$ph] =
00282                 self::index_replace( $contents, $ret, $start, $end );
00283         }
00284 
00285         $prettyTemplate = $text;
00286         foreach ( $tagPlaceHolders as $ph => $value ) {
00287             $prettyTemplate = str_replace( $ph, '[...]', $prettyTemplate );
00288         }
00289 
00290         if ( strpos( $text, '<translate>' ) !== false ) {
00291             throw new TPException( array( 'pt-parse-open', $prettyTemplate ) );
00292         } elseif ( strpos( $text, '</translate>' ) !== false ) {
00293             throw new TPException( array( 'pt-parse-close', $prettyTemplate ) );
00294         }
00295 
00296         foreach ( $tagPlaceHolders as $ph => $value ) {
00297             $text = str_replace( $ph, $value, $text );
00298         }
00299 
00300         if ( count( $sections ) === 1 ) {
00301             // Don't return display title for pages which have no sections
00302             $sections = array();
00303         }
00304 
00305         $text = self::unArmourNowiki( $nowiki, $text );
00306 
00307         $parse = new TPParse( $this->getTitle() );
00308         $parse->template = $text;
00309         $parse->sections = $sections;
00310 
00311         // Cache it
00312         $this->cachedParse = $parse;
00313 
00314         return $parse;
00315     }
00316 
00317     // Inner functionality //
00318 
00324     public static function armourNowiki( &$holders, $text ) {
00325         $re = '~(<nowiki>)(.*?)(</nowiki>)~s';
00326 
00327         while ( preg_match( $re, $text, $matches ) ) {
00328             $ph = TranslateUtils::getPlaceholder();
00329             $text = str_replace( $matches[0], $ph, $text );
00330             $holders[$ph] = $matches[0];
00331         }
00332 
00333         return $text;
00334     }
00335 
00341     public static function unArmourNowiki( $holders, $text ) {
00342         foreach ( $holders as $ph => $value ) {
00343             $text = str_replace( $ph, $value, $text );
00344         }
00345 
00346         return $text;
00347     }
00348 
00356     protected static function index_replace( $string, $rep, $start, $end ) {
00357         return substr( $string, 0, $start ) . $rep . substr( $string, $end );
00358     }
00359 
00368     protected function sectionise( &$sections, $text ) {
00369         $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE;
00370         $parts = preg_split( '~(\s*\n\n\s*|\s*$)~', $text, -1, $flags );
00371 
00372         $template = '';
00373         foreach ( $parts as $_ ) {
00374             if ( trim( $_ ) === '' ) {
00375                 $template .= $_;
00376             } else {
00377                 $ph = TranslateUtils::getPlaceholder();
00378                 $sections[$ph] = $this->shakeSection( $_ );
00379                 $template .= $ph;
00380             }
00381         }
00382 
00383         return $template;
00384     }
00385 
00398     protected function shakeSection( $content ) {
00399         $re = '~<!--T:(.*?)-->~';
00400         $matches = array();
00401         $count = preg_match_all( $re, $content, $matches, PREG_SET_ORDER );
00402 
00403         if ( $count > 1 ) {
00404             throw new TPException( array( 'pt-shake-multiple', $content ) );
00405         }
00406 
00407         $section = new TPSection;
00408         if ( $count === 1 ) {
00409             foreach ( $matches as $match ) {
00410                 list( /*full*/, $id ) = $match;
00411                 $section->id = $id;
00412 
00413                 // Currently handle only these two standard places.
00414                 // Is this too strict?
00415                 $rer1 = '~^<!--T:(.*?)-->\n~'; // Normal sections
00416                 $rer2 = '~\s*<!--T:(.*?)-->$~m'; // Sections with title
00417                 $content = preg_replace( $rer1, '', $content );
00418                 $content = preg_replace( $rer2, '', $content );
00419 
00420                 if ( preg_match( $re, $content ) === 1 ) {
00421                     throw new TPException( array( 'pt-shake-position', $content ) );
00422                 } elseif ( trim( $content ) === '' ) {
00423                     throw new TPException( array( 'pt-shake-empty', $id ) );
00424                 }
00425             }
00426         } else {
00427             // New section
00428             $section->id = -1;
00429         }
00430 
00431         $section->text = $content;
00432 
00433         return $section;
00434     }
00435 
00436     // Tag methods //
00437 
00438     protected static $tagCache = array();
00439 
00446     public function addMarkedTag( $revision, $value = null ) {
00447         $this->addTag( 'tp:mark', $revision, $value );
00448         MessageGroups::clearCache();
00449     }
00450 
00456     public function addReadyTag( $revision ) {
00457         $this->addTag( 'tp:tag', $revision );
00458     }
00459 
00466     protected function addTag( $tag, $revision, $value = null ) {
00467         $dbw = wfGetDB( DB_MASTER );
00468 
00469         $aid = $this->getTitle()->getArticleID();
00470 
00471         if ( is_object( $revision ) ) {
00472             throw new MWException( 'Got object, excepted id' );
00473         }
00474 
00475         $conds = array(
00476             'rt_page' => $aid,
00477             'rt_type' => RevTag::getType( $tag ),
00478             'rt_revision' => $revision
00479         );
00480         $dbw->delete( 'revtag', $conds, __METHOD__ );
00481 
00482         if ( $value !== null ) {
00483             $conds['rt_value'] = serialize( implode( '|', $value ) );
00484         }
00485 
00486         $dbw->insert( 'revtag', $conds, __METHOD__ );
00487 
00488         self::$tagCache[$aid][$tag] = $revision;
00489     }
00490 
00495     public function getMarkedTag() {
00496         return $this->getTag( 'tp:mark' );
00497     }
00498 
00503     public function getReadyTag() {
00504         return $this->getTag( 'tp:tag' );
00505     }
00506 
00511     public function unmarkTranslatablePage() {
00512         $aid = $this->getTitle()->getArticleID();
00513 
00514         $dbw = wfGetDB( DB_MASTER );
00515         $conds = array(
00516             'rt_page' => $aid,
00517             'rt_type' => array(
00518                 RevTag::getType( 'tp:mark' ),
00519                 RevTag::getType( 'tp:tag' ),
00520             ),
00521         );
00522 
00523         $dbw->delete( 'revtag', $conds, __METHOD__ );
00524         $dbw->delete( 'translate_sections', array( 'trs_page' => $aid ), __METHOD__ );
00525         unset( self::$tagCache[$aid] );
00526     }
00527 
00533     protected function getTag( $tag, $dbt = DB_SLAVE ) {
00534         if ( !$this->getTitle()->exists() ) {
00535             return false;
00536         }
00537 
00538         $aid = $this->getTitle()->getArticleID();
00539 
00540         if ( isset( self::$tagCache[$aid][$tag] ) ) {
00541             return self::$tagCache[$aid][$tag];
00542         }
00543 
00544         $db = wfGetDB( $dbt );
00545 
00546         $conds = array(
00547             'rt_page' => $aid,
00548             'rt_type' => RevTag::getType( $tag ),
00549         );
00550 
00551         $options = array( 'ORDER BY' => 'rt_revision DESC' );
00552 
00553         // Tag values are not stored, only the associated revision
00554         $tagRevision = $db->selectField( 'revtag', 'rt_revision', $conds, __METHOD__, $options );
00555         if ( $tagRevision !== false ) {
00556             return self::$tagCache[$aid][$tag] = intval( $tagRevision );
00557         } else {
00558             return self::$tagCache[$aid][$tag] = false;
00559         }
00560     }
00561 
00567     public function getTranslationUrl( $code = false ) {
00568         $params = array(
00569             'group' => $this->getMessageGroupId(),
00570             'action' => 'page',
00571             'filter' => '',
00572             'language' => $code,
00573         );
00574 
00575         $translate = SpecialPage::getTitleFor( 'Translate' );
00576 
00577         return $translate->getLocalURL( $params );
00578     }
00579 
00580     public function getMarkedRevs() {
00581         // Avoid replication lag issues
00582         $db = wfGetDB( DB_MASTER );
00583 
00584         $fields = array( 'rt_revision', 'rt_value' );
00585         $conds = array(
00586             'rt_page' => $this->getTitle()->getArticleID(),
00587             'rt_type' => RevTag::getType( 'tp:mark' ),
00588         );
00589         $options = array( 'ORDER BY' => 'rt_revision DESC' );
00590 
00591         return $db->select( 'revtag', $fields, $conds, __METHOD__, $options );
00592     }
00593 
00598     public function getTranslationPages() {
00599         // Avoid replication lag issues
00600         $dbr = wfGetDB( DB_MASTER );
00601         $prefix = $this->getTitle()->getDBkey() . '/';
00602         $likePattern = $dbr->buildLike( $prefix, $dbr->anyString() );
00603         $res = $dbr->select(
00604             'page',
00605             array( 'page_namespace', 'page_title' ),
00606             array(
00607                 'page_namespace' => $this->getTitle()->getNamespace(),
00608                 "page_title $likePattern"
00609             ),
00610             __METHOD__
00611         );
00612 
00613         $titles = TitleArray::newFromResult( $res );
00614         $filtered = array();
00615 
00616         // Make sure we only get translation subpages while ignoring others
00617         $codes = Language::getLanguageNames( false );
00618         $prefix = $this->getTitle()->getText();
00620         foreach ( $titles as $title ) {
00621             list( $name, $code ) = TranslateUtils::figureMessage( $title->getText() );
00622             if ( !isset( $codes[$code] ) || $name !== $prefix ) {
00623                 continue;
00624             }
00625             $filtered[] = $title;
00626         }
00627 
00628         return $filtered;
00629     }
00630 
00636     protected function getSections() {
00637         $dbw = wfGetDB( DB_MASTER );
00638         $conds = array( 'trs_page' => $this->getTitle()->getArticleID() );
00639         $res = $dbw->select( 'translate_sections', 'trs_key', $conds, __METHOD__ );
00640 
00641         $sections = array();
00642         foreach ( $res as $row ) {
00643             $sections[] = $row->trs_key;
00644         }
00645 
00646         return $sections;
00647     }
00648 
00656     public function getTranslationUnitPages( $set = 'active', $code = false ) {
00657         $dbw = wfGetDB( DB_MASTER );
00658         $base = $this->getTitle()->getPrefixedDBKey();
00659         // Including the / used as separator
00660         $baseLength = strlen( $base ) + 1;
00661 
00662         if ( $code !== false ) {
00663             $like = $dbw->buildLike( "$base/", $dbw->anyString(), "/$code" );
00664         } else {
00665             $like = $dbw->buildLike( "$base/", $dbw->anyString() );
00666         }
00667 
00668         $fields = array( 'page_namespace', 'page_title' );
00669         $conds = array(
00670             'page_namespace' => NS_TRANSLATIONS,
00671             'page_title ' . $like
00672         );
00673         $res = $dbw->select( 'page', $fields, $conds, __METHOD__ );
00674 
00675         // Only include pages which belong to this translatable page.
00676         // Problematic cases are when pages Foo and Foo/bar are both
00677         // translatable. Then when querying for Foo, we also get units
00678         // belonging to Foo/bar.
00679         $sections = array_flip( $this->getSections() );
00680         $units = array();
00681         foreach ( $res as $row ) {
00682             $title = Title::newFromRow( $row );
00683 
00684             // Strip the language code and the name of the
00685             // translatable to get plain section key
00686             $handle = new MessageHandle( $title );
00687             $key = substr( $handle->getKey(), $baseLength );
00688             if ( strpos( $key, '/' ) !== false ) {
00689                 // Probably belongs to translatable subpage
00690                 continue;
00691             }
00692 
00693             // Check against list of sections if requested
00694             if ( $set === 'active' && !isset( $sections[$key] ) ) {
00695                 continue;
00696             }
00697 
00698             // We have a match :)
00699             $units[] = $title;
00700         }
00701 
00702         return $units;
00703     }
00704 
00709     public function getTranslationPercentages() {
00710         // Calculate percentages for the available translations
00711         $group = $this->getMessageGroup();
00712         if ( !$group instanceof WikiPageMessageGroup ) {
00713             return array();
00714         }
00715 
00716         $titles = $this->getTranslationPages();
00717         $temp = MessageGroupStats::forGroup( $this->getMessageGroupId() );
00718         $stats = array();
00719 
00720         foreach ( $titles as $t ) {
00721             $handle = new MessageHandle( $t );
00722             $code = $handle->getCode();
00723 
00724             // Sometimes we want to display 0.00 for pages for which translation
00725             // hasn't started yet.
00726             $stats[$code] = 0.00;
00727             if ( isset( $temp[$code] ) && $temp[$code][MessageGroupStats::TOTAL] > 0 ) {
00728                 $total = $temp[$code][MessageGroupStats::TOTAL];
00729                 $translated = $temp[$code][MessageGroupStats::TRANSLATED];
00730                 $percentage = $translated / $total;
00731                 $stats[$code] = sprintf( '%.2f', $percentage );
00732             }
00733         }
00734 
00735         // Content language is always up-to-date
00736         $stats[$this->getSourceLanguageCode()] = 1.00;
00737 
00738         return $stats;
00739     }
00740 
00741     public function getTransRev( $suffix ) {
00742         $title = Title::makeTitle( NS_TRANSLATIONS, $suffix );
00743 
00744         // Avoid replication lag issues
00745         $db = wfGetDB( DB_MASTER );
00746         $fields = 'rt_value';
00747         $conds = array(
00748             'rt_page' => $title->getArticleID(),
00749             'rt_type' => RevTag::getType( 'tp:transver' ),
00750         );
00751         $options = array( 'ORDER BY' => 'rt_revision DESC' );
00752 
00753         return $db->selectField( 'revtag', $fields, $conds, __METHOD__, $options );
00754     }
00755 
00760     public static function isTranslationPage( Title $title ) {
00761         $handle = new MessageHandle( $title );
00762         $key = $handle->getKey();
00763         $code = $handle->getCode();
00764 
00765         if ( $key === '' || $code === '' ) {
00766             return false;
00767         }
00768 
00769         $codes = Language::getLanguageNames( false );
00770         global $wgTranslateDocumentationLanguageCode;
00771         unset( $codes[$wgTranslateDocumentationLanguageCode] );
00772 
00773         if ( !isset( $codes[$code] ) ) {
00774             return false;
00775         }
00776 
00777         $newtitle = self::changeTitleText( $title, $key );
00778 
00779         if ( !$newtitle ) {
00780             return false;
00781         }
00782 
00783         $page = TranslatablePage::newFromTitle( $newtitle );
00784 
00785         if ( $page->getMarkedTag() === false ) {
00786             return false;
00787         }
00788 
00789         return $page;
00790     }
00791 
00792     protected static function changeTitleText( Title $title, $text ) {
00793         return Title::makeTitleSafe( $title->getNamespace(), $text );
00794     }
00795 
00800     public static function isSourcePage( Title $title ) {
00801         static $cache = null;
00802 
00803         $cacheObj = wfGetCache( CACHE_ANYTHING );
00804         $cacheKey = wfMemcKey( 'pagetranslation', 'sourcepages' );
00805 
00806         if ( $cache === null ) {
00807             $cache = $cacheObj->get( $cacheKey );
00808         }
00809         if ( !is_array( $cache ) ) {
00810             $cache = self::getTranslatablePages();
00811             $cacheObj->set( $cacheKey, $cache, 60 * 5 );
00812         }
00813 
00814         return in_array( $title->getArticleID(), $cache );
00815     }
00816 
00820     public static function getTranslatablePages() {
00821         // Avoid replication lag issues
00822         $dbr = wfGetDB( DB_MASTER );
00823 
00824         $tables = array( 'revtag', 'page' );
00825         $fields = 'rt_page';
00826         $conds = array(
00827             'rt_page = page_id',
00828             'rt_revision = page_latest',
00829             'rt_type' => array( RevTag::getType( 'tp:mark' ), RevTag::getType( 'tp:tag' ) ),
00830         );
00831         $options = array( 'GROUP BY' => 'rt_page' );
00832 
00833         $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options );
00834         $results = array();
00835         foreach ( $res as $row ) {
00836             $results[] = $row->rt_page;
00837         }
00838 
00839         return $results;
00840     }
00841 }
Generated on Tue Oct 29 00:00:24 2013 for MediaWiki Translate Extension by  doxygen 1.6.3