MessageCollection.php

Go to the documentation of this file.
00001 <?php
00019 class MessageCollection implements ArrayAccess, Iterator, Countable {
00023     public $code;
00024 
00028     protected $definitions = null;
00029 
00033     protected $infile = array();
00034 
00035     // Keys and messages.
00036 
00040     protected $keys = array();
00041 
00045     protected $messages = array();
00046 
00050     protected $reverseMap;
00051 
00052     // Database resources
00053 
00055     protected $dbInfo = null;
00056 
00058     protected $dbData = null;
00059 
00061     protected $dbReviewData = array();
00062 
00067     protected $tags = array();
00068 
00072     protected $properties = array();
00073 
00077     protected $authors = array();
00078 
00083     public function __construct( $code ) {
00084         $this->code = $code;
00085     }
00086 
00093     public static function newFromDefinitions( MessageDefinitions $definitions, $code ) {
00094         $collection = new self( $code );
00095         $collection->definitions = $definitions;
00096         $collection->resetForNewLanguage( $code );
00097 
00098         return $collection;
00099     }
00100 
00106     public static function newEmpty( $code ) {
00107     }
00108 
00112     public function getLanguage() {
00113         return $this->code;
00114     }
00115 
00116     // Data setters
00117 
00124     public function setInfile( array $messages ) {
00125         $this->infile = $messages;
00126     }
00127 
00133     public function setTags( $type, array $keys ) {
00134         $this->tags[$type] = $keys;
00135     }
00136 
00141     public function keys() {
00142         return $this->keys;
00143     }
00144 
00150     public function getTitles() {
00151         return array_values( $this->keys );
00152     }
00153 
00159     public function getMessageKeys() {
00160         return array_keys( $this->keys );
00161     }
00162 
00168     public function getTags( $type ) {
00169         return isset( $this->tags[$type] ) ? $this->tags[$type] : array();
00170     }
00171 
00178     public function getAuthors() {
00179         $this->loadTranslations();
00180 
00181         $authors = array_flip( $this->authors );
00182 
00183         foreach ( $this->messages as $m ) {
00184             // Check if there are authors
00188             $author = $m->getProperty( 'last-translator-text' );
00189 
00190             if ( $author === null ) {
00191                 continue;
00192             }
00193 
00194             if ( !isset( $authors[$author] ) ) {
00195                 $authors[$author] = 1;
00196             } else {
00197                 $authors[$author]++;
00198             }
00199         }
00200 
00201         # arsort( $authors, SORT_NUMERIC );
00202         ksort( $authors );
00203         $fuzzyBot = FuzzyBot::getName();
00204         foreach ( $authors as $author => $edits ) {
00205             if ( $author !== $fuzzyBot ) {
00206                 $filteredAuthors[] = $author;
00207             }
00208         }
00209 
00210         return isset( $filteredAuthors ) ? $filteredAuthors : array();
00211     }
00212 
00219     public function addCollectionAuthors( $authors, $mode = 'append' ) {
00220         switch ( $mode ) {
00221             case 'append':
00222                 $authors = array_merge( $this->authors, $authors );
00223                 break;
00224             case 'set':
00225                 break;
00226             default:
00227                 throw new MWException( "Invalid mode $mode" );
00228         }
00229 
00230         $this->authors = array_unique( $authors );
00231     }
00232 
00236     public function setReviewMode( $value = true ) {
00237     }
00238 
00239     // Data modifiers
00240 
00247     public function loadTranslations( $dbtype = DB_SLAVE ) {
00248         $this->loadData( $this->keys, $dbtype );
00249         $this->loadInfo( $this->keys, $dbtype );
00250         $this->loadReviewInfo( $this->keys, $dbtype );
00251         $this->initMessages();
00252     }
00253 
00259     public function resetForNewLanguage( $code ) {
00260         $this->code = $code;
00261         $this->keys = $this->fixKeys();
00262         $this->dbInfo = null;
00263         $this->dbData = null;
00264         $this->dbReviewData = array();
00265         $this->messages = null;
00266         $this->infile = array();
00267         $this->authors = array();
00268 
00269         unset( $this->tags['fuzzy'] );
00270         $this->reverseMap = null;
00271         $this->getReverseMap();
00272     }
00273 
00281     public function slice( $offset, $limit ) {
00282         $indexes = array_keys( $this->keys );
00283 
00284         if ( $offset === '' ) {
00285             $offset = 0;
00286         }
00287 
00288         // Handle string offsets
00289         if ( !ctype_digit( strval( $offset ) ) ) {
00290             $count = 0;
00291             foreach ( array_keys( $this->keys ) as $index ) {
00292                 if ( $index === $offset ) {
00293                     break;
00294                 }
00295                 $count++;
00296             }
00297             // Now offset is always an integer, suitable for array_slice
00298             $offset = $count;
00299         }
00300 
00301         // False means that cannot go back or forward
00302         $backwardsOffset = $forwardsOffset = false;
00303         // Backwards paging uses numerical indexes, see below
00304 
00305         // Can only skip this if no offset has been provided or the
00306         // offset is zero. (offset - limit ) > 1 does not work, because
00307         // users can end in offest=2, limit=5 and can't see the first
00308         // two messages. That's also why it is capped into zero with
00309         // max(). And finally make the offsets to be strings even if
00310         // they are numbers in this case.
00311         if ( $offset > 0 ) {
00312             $backwardsOffset = strval( max( 0, $offset - $limit ) );
00313         }
00314 
00315         // Forwards paging uses keys. If user opens view Untranslated,
00316         // translates some messages and then clicks next, the first
00317         // message visible in the page is the first message not shown
00318         // in the previous page (unless someone else translated it at
00319         // the same time). If we used integer offsets, we would skip
00320         // same number of messages that were translated, because they
00321         // are no longer in the list. For backwards paging this is not
00322         // such a big issue, so it still uses integer offsets, because
00323         // we would need to also implement "direction" to have it work
00324         // correctly.
00325         if ( isset( $indexes[$offset + $limit] ) ) {
00326             $forwardsOffset = $indexes[$offset + $limit];
00327         }
00328 
00329         $this->keys = array_slice( $this->keys, $offset, $limit, true );
00330 
00331         return array( $backwardsOffset, $forwardsOffset, $offset );
00332     }
00333 
00357     public function filter( $type, $condition = true, $value = null ) {
00358         if ( !in_array( $type, self::getAvailableFilters(), true ) ) {
00359             throw new MWException( "Unknown filter $type" );
00360         }
00361         $this->applyFilter( $type, $condition, $value );
00362     }
00363 
00367     public static function getAvailableFilters() {
00368         return array(
00369             'fuzzy',
00370             'optional',
00371             'ignored',
00372             'hastranslation',
00373             'changed',
00374             'translated',
00375             'reviewer',
00376             'last-translator',
00377         );
00378     }
00379 
00388     protected function applyFilter( $filter, $condition, $value ) {
00389         $keys = $this->keys;
00390         if ( $filter === 'fuzzy' ) {
00391             $keys = $this->filterFuzzy( $keys, $condition );
00392         } elseif ( $filter === 'hastranslation' ) {
00393             $keys = $this->filterHastranslation( $keys, $condition );
00394         } elseif ( $filter === 'translated' ) {
00395             $fuzzy = $this->filterFuzzy( $keys, false );
00396             $hastranslation = $this->filterHastranslation( $keys, false );
00397             // Fuzzy messages are not counted as translated messages
00398             $translated = $this->filterOnCondition( $hastranslation, $fuzzy );
00399             $keys = $this->filterOnCondition( $keys, $translated, $condition );
00400         } elseif ( $filter === 'changed' ) {
00401             $keys = $this->filterChanged( $keys, $condition );
00402         } elseif ( $filter === 'reviewer' ) {
00403             $keys = $this->filterReviewer( $keys, $condition, $value );
00404         } elseif ( $filter === 'last-translator' ) {
00405             $keys = $this->filterLastTranslator( $keys, $condition, $value );
00406         } else {
00407             // Filter based on tags.
00408             if ( !isset( $this->tags[$filter] ) ) {
00409                 if ( $filter !== 'optional' && $filter !== 'ignored' ) {
00410                     throw new MWException( "No tagged messages for custom filter $filter" );
00411                 }
00412                 $keys = $this->filterOnCondition( $keys, array(), $condition );
00413             } else {
00414                 $taggedKeys = array_flip( $this->tags[$filter] );
00415                 $keys = $this->filterOnCondition( $keys, $taggedKeys, $condition );
00416             }
00417         }
00418 
00419         $this->keys = $keys;
00420     }
00421 
00437     protected function filterOnCondition( array $keys, array $condKeys, $condition = true ) {
00438         if ( $condition === true ) {
00439             // Delete $condKeys from $keys
00440             foreach ( array_keys( $condKeys ) as $key ) {
00441                 unset( $keys[$key] );
00442             }
00443         } else {
00444             // Keep the keys which are in $condKeys
00445             foreach ( array_keys( $keys ) as $key ) {
00446                 if ( !isset( $condKeys[$key] ) ) {
00447                     unset( $keys[$key] );
00448                 }
00449             }
00450         }
00451 
00452         return $keys;
00453     }
00454 
00462     protected function filterFuzzy( array $keys, $condition ) {
00463         $this->loadInfo( $keys );
00464 
00465         $origKeys = array();
00466         if ( $condition === false ) {
00467             $origKeys = $keys;
00468         }
00469 
00470         foreach ( $this->dbInfo as $row ) {
00471             if ( $row->rt_type !== null ) {
00472                 unset( $keys[$this->rowToKey( $row )] );
00473             }
00474         }
00475 
00476         if ( $condition === false ) {
00477             $keys = array_diff( $origKeys, $keys );
00478         }
00479 
00480         return $keys;
00481     }
00482 
00490     protected function filterHastranslation( array $keys, $condition ) {
00491         $this->loadInfo( $keys );
00492 
00493         $origKeys = array();
00494         if ( $condition === false ) {
00495             $origKeys = $keys;
00496         }
00497 
00498         foreach ( $this->dbInfo as $row ) {
00499             unset( $keys[$this->rowToKey( $row )] );
00500         }
00501 
00502         // Check also if there is something in the file that is not yet in the database
00503         foreach ( array_keys( $this->infile ) as $inf ) {
00504             unset( $keys[$inf] );
00505         }
00506 
00507         // Remove the messages which do not have a translation from the list
00508         if ( $condition === false ) {
00509             $keys = array_diff( $origKeys, $keys );
00510         }
00511 
00512         return $keys;
00513     }
00514 
00523     protected function filterChanged( array $keys, $condition ) {
00524         $this->loadData( $keys );
00525 
00526         $origKeys = array();
00527         if ( $condition === false ) {
00528             $origKeys = $keys;
00529         }
00530 
00531         foreach ( $this->dbData as $row ) {
00532             $mkey = $this->rowToKey( $row );
00533             if ( !isset( $this->infile[$mkey] ) ) {
00534                 continue;
00535             }
00536 
00537             $text = Revision::getRevisionText( $row );
00538             if ( $this->infile[$mkey] === $text ) {
00539                 // Remove unchanged messages from the list
00540                 unset( $keys[$mkey] );
00541             }
00542         }
00543 
00544         // Remove the messages which have not changed from the list
00545         if ( $condition === false ) {
00546             $keys = $this->filterOnCondition( $keys, $origKeys, false );
00547         }
00548 
00549         return $keys;
00550     }
00551 
00560     protected function filterReviewer( array $keys, $condition, $user ) {
00561         $this->loadReviewInfo( $keys );
00562         $origKeys = $keys;
00563 
00564         /* This removes messages from the list which have certain
00565          * reviewer (among others) */
00566         $userId = intval( $user );
00567         foreach ( $this->dbReviewData as $row ) {
00568             if ( $user === null || intval( $row->trr_user ) === $userId ) {
00569                 unset( $keys[$this->rowToKey( $row )] );
00570             }
00571         }
00572 
00573         if ( $condition === false ) {
00574             $keys = array_diff( $origKeys, $keys );
00575         }
00576 
00577         return $keys;
00578     }
00579 
00587     protected function filterLastTranslator( array $keys, $condition, $user ) {
00588         $this->loadData( $keys );
00589         $origKeys = $keys;
00590 
00591         $user = intval( $user );
00592         foreach ( $this->dbData as $row ) {
00593             if ( intval( $row->rev_user ) === $user ) {
00594                 unset( $keys[$this->rowToKey( $row )] );
00595             }
00596         }
00597 
00598         if ( $condition === false ) {
00599             $keys = array_diff( $origKeys, $keys );
00600         }
00601 
00602         return $keys;
00603     }
00604 
00609     protected function fixKeys() {
00610         $newkeys = array();
00611         // array( namespace, pagename )
00612         $pages = $this->definitions->getPages();
00613         $code = $this->code;
00614 
00615         foreach ( $pages as $key => $page ) {
00616             list ( $namespace, $pagename ) = $page;
00617             $title = Title::makeTitleSafe( $namespace, "$pagename/$code" );
00618             if ( !$title ) {
00619                 wfWarn( "Invalid title $namespace:$pagename/$code" );
00620                 continue;
00621             }
00622             $newkeys[$key] = $title;
00623         }
00624 
00625         return $newkeys;
00626     }
00627 
00633     protected function loadInfo( array $keys, $dbtype = DB_SLAVE ) {
00634         if ( $this->dbInfo !== null ) {
00635             return;
00636         }
00637 
00638         $this->dbInfo = array();
00639 
00640         if ( !count( $keys ) ) {
00641             return;
00642         }
00643 
00644         $dbr = wfGetDB( $dbtype );
00645         $tables = array( 'page', 'revtag' );
00646         $fields = array( 'page_namespace', 'page_title', 'rt_type' );
00647         $conds = $this->getTitleConds( $dbr );
00648         $joins = array( 'revtag' =>
00649         array(
00650             'LEFT JOIN',
00651             array( 'page_id=rt_page', 'page_latest=rt_revision', 'rt_type' => RevTag::getType( 'fuzzy' ) )
00652         )
00653         );
00654 
00655         $this->dbInfo = $dbr->select( $tables, $fields, $conds, __METHOD__, array(), $joins );
00656     }
00657 
00663     protected function loadReviewInfo( array $keys, $dbtype = DB_SLAVE ) {
00664         if ( $this->dbReviewData !== array() ) {
00665             return;
00666         }
00667 
00668         $this->dbReviewData = array();
00669 
00670         if ( !count( $keys ) ) {
00671             return;
00672         }
00673 
00674         $dbr = wfGetDB( $dbtype );
00675         $tables = array( 'page', 'translate_reviews' );
00676         $fields = array( 'page_namespace', 'page_title', 'trr_user' );
00677         $conds = $this->getTitleConds( $dbr );
00678         $joins = array( 'translate_reviews' =>
00679         array(
00680             'JOIN',
00681             array( 'page_id=trr_page', 'page_latest=trr_revision' )
00682         )
00683         );
00684 
00685         $this->dbReviewData = $dbr->select( $tables, $fields, $conds, __METHOD__, array(), $joins );
00686     }
00687 
00693     protected function loadData( array $keys, $dbtype = DB_SLAVE ) {
00694         if ( $this->dbData !== null ) {
00695             return;
00696         }
00697 
00698         $this->dbData = array();
00699 
00700         if ( !count( $keys ) ) {
00701             return;
00702         }
00703 
00704         $dbr = wfGetDB( $dbtype );
00705 
00706         $tables = array( 'page', 'revision', 'text' );
00707         $fields = array(
00708             'page_namespace',
00709             'page_title',
00710             'page_latest',
00711             'rev_user',
00712             'rev_user_text',
00713             'old_flags',
00714             'old_text'
00715         );
00716         $conds = array(
00717             'page_latest = rev_id',
00718             'old_id = rev_text_id',
00719         );
00720         $conds[] = $this->getTitleConds( $dbr );
00721 
00722         $res = $dbr->select( $tables, $fields, $conds, __METHOD__ );
00723 
00724         $this->dbData = $res;
00725     }
00726 
00733     protected function getTitleConds( $db ) {
00734         // Array of array( namespace, pagename )
00735         $byNamespace = array();
00736         foreach ( $this->getTitles() as $title ) {
00737             $namespace = $title->getNamespace();
00738             $pagename = $title->getDBKey();
00739             $byNamespace[$namespace][] = $pagename;
00740         }
00741 
00742         $conds = array();
00743         foreach ( $byNamespace as $namespaces => $pagenames ) {
00744             $cond = array(
00745                 'page_namespace' => $namespaces,
00746                 'page_title' => $pagenames,
00747             );
00748 
00749             $conds[] = $db->makeList( $cond, LIST_AND );
00750         }
00751 
00752         return $db->makeList( $conds, LIST_OR );
00753     }
00754 
00761     protected function rowToKey( $row ) {
00762         $map = $this->getReverseMap();
00763         if ( isset( $map[$row->page_namespace][$row->page_title] ) ) {
00764             return $map[$row->page_namespace][$row->page_title];
00765         } else {
00766             wfWarn( "Got unknown title from the database: {$row->page_namespace}:{$row->page_title}" );
00767 
00768             return null;
00769         }
00770     }
00771 
00776     public function getReverseMap() {
00777         if ( isset( $this->reverseMap ) ) {
00778             return $this->reverseMap;
00779         }
00780 
00781         $map = array();
00785         foreach ( $this->keys as $mkey => $title ) {
00786             $map[$title->getNamespace()][$title->getDBKey()] = $mkey;
00787         }
00788 
00789         return $this->reverseMap = $map;
00790     }
00791 
00796     public function initMessages() {
00797         if ( $this->messages !== null ) {
00798             return;
00799         }
00800 
00801         $messages = array();
00802         $definitions = $this->definitions->getDefinitions();
00803         foreach ( array_keys( $this->keys ) as $mkey ) {
00804             $messages[$mkey] = new ThinMessage( $mkey, $definitions[$mkey] );
00805         }
00806 
00807         // Copy rows if any.
00808         if ( $this->dbData !== null ) {
00809             foreach ( $this->dbData as $row ) {
00810                 $mkey = $this->rowToKey( $row );
00811                 if ( !isset( $messages[$mkey] ) ) {
00812                     continue;
00813                 }
00814                 $messages[$mkey]->setRow( $row );
00815                 $messages[$mkey]->setProperty( 'revision', $row->page_latest );
00816             }
00817         }
00818 
00819         if ( $this->dbInfo !== null ) {
00820             $fuzzy = array();
00821             foreach ( $this->dbInfo as $row ) {
00822                 if ( $row->rt_type !== null ) {
00823                     $fuzzy[] = $this->rowToKey( $row );
00824                 }
00825             }
00826 
00827             $this->setTags( 'fuzzy', $fuzzy );
00828         }
00829 
00830         // Copy tags if any.
00831         foreach ( $this->tags as $type => $keys ) {
00832             foreach ( $keys as $mkey ) {
00833                 if ( isset( $messages[$mkey] ) ) {
00834                     $messages[$mkey]->addTag( $type );
00835                 }
00836             }
00837         }
00838 
00839         // Copy properties if any.
00840         foreach ( $this->properties as $type => $keys ) {
00841             foreach ( $keys as $mkey => $value ) {
00842                 if ( isset( $messages[$mkey] ) ) {
00843                     $messages[$mkey]->setProperty( $type, $value );
00844                 }
00845             }
00846         }
00847 
00848         // Copy infile if any.
00849         foreach ( $this->infile as $mkey => $value ) {
00850             if ( isset( $messages[$mkey] ) ) {
00851                 $messages[$mkey]->setInfile( $value );
00852             }
00853         }
00854 
00855         foreach ( $this->dbReviewData as $row ) {
00856             $mkey = $this->rowToKey( $row );
00857             if ( !isset( $messages[$mkey] ) ) {
00858                 continue;
00859             }
00860             $messages[$mkey]->appendProperty( 'reviewers', $row->trr_user );
00861         }
00862 
00863         // Set the status property
00864         foreach ( $messages as $obj ) {
00865             if ( $obj->hasTag( 'fuzzy' ) ) {
00866                 $obj->setProperty( 'status', 'fuzzy' );
00867             } elseif ( is_array( $obj->getProperty( 'reviewers' ) ) ) {
00868                 $obj->setProperty( 'status', 'proofread' );
00869             } elseif ( $obj->translation() !== null ) {
00870                 $obj->setProperty( 'status', 'translated' );
00871             } else {
00872                 $obj->setProperty( 'status', 'untranslated' );
00873             }
00874         }
00875 
00876         $this->messages = $messages;
00877     }
00878 
00884     public function offsetExists( $offset ) {
00885         return isset( $this->keys[$offset] );
00886     }
00887 
00892     public function offsetGet( $offset ) {
00893         return $this->messages[$offset];
00894     }
00895 
00900     public function offsetSet( $offset, $value ) {
00901         $this->messages[$offset] = $value;
00902     }
00903 
00907     public function offsetUnset( $offset ) {
00908         unset( $this->keys[$offset] );
00909     }
00910 
00918     public function __get( $name ) {
00919         throw new MWException( __METHOD__ . ": Trying to access unknown property $name" );
00920     }
00921 
00922     public function __set( $name, $value ) {
00923         throw new MWException( __METHOD__ . ": Trying to modify unknown property $name" );
00924     }
00925 
00931     public function rewind() {
00932         reset( $this->keys );
00933     }
00934 
00935     public function current() {
00936         if ( !count( $this->keys ) ) {
00937             return false;
00938         }
00939 
00940         return $this->messages[key( $this->keys )];
00941     }
00942 
00943     public function key() {
00944         return key( $this->keys );
00945     }
00946 
00947     public function next() {
00948         return next( $this->keys );
00949     }
00950 
00951     public function valid() {
00952         return isset( $this->messages[key( $this->keys )] );
00953     }
00954 
00955     public function count() {
00956         return count( $this->keys() );
00957     }
00959 }
00960 
00966 class MessageDefinitions {
00967     protected $namespace;
00968     protected $messages;
00969 
00970     public function __construct( array $messages, $namespace = false ) {
00971         $this->namespace = $namespace;
00972         $this->messages = $messages;
00973     }
00974 
00975     public function getDefinitions() {
00976         return $this->messages;
00977     }
00978 
00982     public function getPages() {
00983         $namespace = $this->namespace;
00984         $pages = array();
00985         foreach ( array_keys( $this->messages ) as $key ) {
00986             if ( $namespace === false ) {
00987                 // pages are in format ex. "8:jan"
00988                 $pages[$key] = explode( ':', $key, 2 );
00989             } else {
00990                 $pages[$key] = array( $namespace, $key );
00991             }
00992         }
00993 
00994         return $pages;
00995     }
00996 }
Generated on Tue Oct 29 00:00:24 2013 for MediaWiki Translate Extension by  doxygen 1.6.3