MessageGroups.php

Go to the documentation of this file.
00001 <?php
00017 class MessageGroups {
00021     protected static $prioritycache = null;
00022 
00023     protected static $groups = null;
00024 
00026     public static function init() {
00027         if ( is_array( self::$groups ) ) {
00028             return;
00029         }
00030 
00031         wfProfileIn( __METHOD__ );
00032         self::$groups = array();
00033 
00034         global $wgAutoloadClasses;
00035 
00036         $key = wfMemcKey( 'translate-groups' );
00037         $value = DependencyWrapper::getValueFromCache( self::getCache(), $key );
00038 
00039         if ( $value === null ) {
00040             wfDebug( __METHOD__ . "-nocache\n" );
00041             self::loadGroupDefinitions();
00042         } else {
00043             wfDebug( __METHOD__ . "-withcache\n" );
00044             self::$groups = $value['cc'];
00045 
00046             foreach ( $value['autoload'] as $class => $file ) {
00047                 $wgAutoloadClasses[$class] = $file;
00048             }
00049         }
00050         wfProfileOut( __METHOD__ );
00051     }
00052 
00058     public static function clearCache() {
00059         $key = wfMemckey( 'translate-groups' );
00060         self::getCache()->delete( $key );
00061         self::$groups = null;
00062     }
00063 
00068     protected static function getCache() {
00069         return wfGetCache( CACHE_ANYTHING );
00070     }
00071 
00079     protected static function loadGroupDefinitions() {
00080         wfProfileIn( __METHOD__ );
00081 
00082         global $wgEnablePageTranslation, $wgTranslateGroupFiles;
00083         global $wgTranslateCC, $wgAutoloadClasses, $wgTranslateWorkflowStates;
00084 
00085         $deps = array();
00086         $deps[] = new GlobalDependency( 'wgEnablePageTranslation' );
00087         $deps[] = new GlobalDependency( 'wgTranslateGroupFiles' );
00088         $deps[] = new GlobalDependency( 'wgTranslateCC' );
00089         $deps[] = new GlobalDependency( 'wgTranslateExtensionDirectory' );
00090         $deps[] = new GlobalDependency( 'wgTranslateWorkflowStates' );
00091 
00092         self::$groups = $wgTranslateCC;
00093 
00094         if ( $wgEnablePageTranslation ) {
00095             wfProfileIn( __METHOD__ . '-pt' );
00096             $dbr = wfGetDB( DB_MASTER );
00097 
00098             $tables = array( 'page', 'revtag' );
00099             $vars = array( 'page_id', 'page_namespace', 'page_title' );
00100             $conds = array( 'page_id=rt_page', 'rt_type' => RevTag::getType( 'tp:mark' ) );
00101             $options = array( 'GROUP BY' => 'rt_page' );
00102             $res = $dbr->select( $tables, $vars, $conds, __METHOD__, $options );
00103 
00104             foreach ( $res as $r ) {
00105                 $title = Title::newFromRow( $r );
00106                 $id = TranslatablePage::getMessageGroupIdFromTitle( $title );
00107                 self::$groups[$id] = new WikiPageMessageGroup( $id, $title );
00108                 self::$groups[$id]->setLabel( $title->getPrefixedText() );
00109             }
00110             wfProfileOut( __METHOD__ . '-pt' );
00111         }
00112 
00113         if ( $wgTranslateWorkflowStates ) {
00114             self::$groups['translate-workflow-states'] = new WorkflowStatesMessageGroup();
00115         }
00116 
00117         wfProfileIn( __METHOD__ . '-hook' );
00118         $autoload = array();
00119         wfRunHooks( 'TranslatePostInitGroups', array( &self::$groups, &$deps, &$autoload ) );
00120         wfProfileOut( __METHOD__ . '-hook' );
00121 
00122         wfProfileIn( __METHOD__ . '-yaml' );
00123         foreach ( $wgTranslateGroupFiles as $configFile ) {
00124             wfDebug( $configFile . "\n" );
00125             $deps[] = new FileDependency( realpath( $configFile ) );
00126             $fgroups = TranslateYaml::parseGroupFile( $configFile );
00127 
00128             foreach ( $fgroups as $id => $conf ) {
00129                 if ( !empty( $conf['AUTOLOAD'] ) && is_array( $conf['AUTOLOAD'] ) ) {
00130                     $dir = dirname( $configFile );
00131                     foreach ( $conf['AUTOLOAD'] as $class => $file ) {
00132                         // For this request and for caching.
00133                         $wgAutoloadClasses[$class] = "$dir/$file";
00134                         $autoload[$class] = "$dir/$file";
00135                     }
00136                 }
00137                 $group = MessageGroupBase::factory( $conf );
00138                 self::$groups[$id] = $group;
00139             }
00140         }
00141         wfProfileOut( __METHOD__ . '-yaml' );
00142 
00143         wfProfileIn( __METHOD__ . '-agg' );
00144         $aggregateGroups = self::getAggregateGroups();
00145         foreach ( $aggregateGroups as $id => $group ) {
00146             self::$groups[$id] = $group;
00147         }
00148         wfProfileOut( __METHOD__ . '-agg' );
00149 
00150         $key = wfMemckey( 'translate-groups' );
00151         $value = array(
00152             'cc' => self::$groups,
00153             'autoload' => $autoload,
00154         );
00155 
00156         wfProfileIn( __METHOD__ . '-save' );
00157         $wrapper = new DependencyWrapper( $value, $deps );
00158         $wrapper->storeToCache( self::getCache(), $key, 60 * 60 * 2 );
00159         wfProfileOut( __METHOD__ . '-save' );
00160 
00161         wfDebug( __METHOD__ . "-end\n" );
00162         wfProfileOut( __METHOD__ );
00163     }
00164 
00170     public static function getGroup( $id ) {
00171         // BC with page| which is now page-
00172         $id = strtr( $id, '|', '-' );
00173         /* Translatable pages use spaces, but MW occasionally likes to
00174          * normalize spaces to underscores */
00175         if ( strpos( $id, 'page-' ) === 0 ) {
00176             $id = strtr( $id, '_', ' ' );
00177         }
00178         self::init();
00179 
00180         if ( isset( self::$groups[$id] ) ) {
00181             if ( is_callable( self::$groups[$id] ) ) {
00182                 return call_user_func( self::$groups[$id], $id );
00183             }
00184 
00185             return self::$groups[$id];
00186         } elseif ( strval( $id ) !== '' && $id[0] === '!' ) {
00187             $dynamic = self::getDynamicGroups();
00188             if ( isset( $dynamic[$id] ) ) {
00189                 return new $dynamic[$id];
00190             }
00191         }
00192 
00193         return null;
00194     }
00195 
00200     public static function exists( $id ) {
00201         return (bool)self::getGroup( $id );
00202     }
00203 
00208     public static function getAllGroups() {
00209         return self::singleton()->getGroups();
00210     }
00211 
00221     public static function getPriority( $group ) {
00222         if ( !isset( self::$prioritycache ) ) {
00223             self::$prioritycache = array();
00224             // Abusing this table originally intented for other purposes
00225             $db = wfGetDB( DB_MASTER );
00226             $table = 'translate_groupreviews';
00227             $fields = array( 'tgr_group', 'tgr_state' );
00228             $conds = array( 'tgr_lang' => '*priority' );
00229             $res = $db->select( $table, $fields, $conds, __METHOD__ );
00230             foreach ( $res as $row ) {
00231                 self::$prioritycache[$row->tgr_group] = $row->tgr_state;
00232             }
00233         }
00234 
00235         if ( $group instanceof MessageGroup ) {
00236             $id = $group->getId();
00237         } else {
00238             $id = $group;
00239         }
00240 
00241         return isset( self::$prioritycache[$id] ) ? self::$prioritycache[$id] : '';
00242     }
00243 
00252     public static function setPriority( $group, $priority = '' ) {
00253         if ( $group instanceof MessageGroup ) {
00254             $id = $group->getId();
00255         } else {
00256             $id = $group;
00257         }
00258 
00259         self::$prioritycache[$id] = $priority;
00260 
00261         $dbw = wfGetDB( DB_MASTER );
00262         $table = 'translate_groupreviews';
00263         $row = array(
00264             'tgr_group' => $id,
00265             'tgr_lang' => '*priority',
00266             'tgr_state' => $priority,
00267         );
00268 
00269         if ( $priority === '' ) {
00270             unset( $row['tgr_state'] );
00271             $dbw->delete( $table, $row, __METHOD__ );
00272         } else {
00273             $index = array( 'tgr_group', 'tgr_lang' );
00274             $dbw->replace( $table, array( $index ), $row, __METHOD__ );
00275         }
00276     }
00277 
00279     public static function isDynamic( MessageGroup $group ) {
00280         $id = $group->getId();
00281 
00282         return strval( $id ) !== '' && $id[0] === '!';
00283     }
00284 
00292     public static function getSharedGroups( MessageGroup $group ) {
00293         // Take the first message, get a handle for it and check
00294         // if that message belongs to other groups. Those are the
00295         // parent aggregate groups. Ideally we loop over all keys,
00296         // but this should be enough.
00297         $keys = array_keys( $group->getDefinitions() );
00298         $title = Title::makeTitle( $group->getNamespace(), $keys[0] );
00299         $handle = new MessageHandle( $title );
00300         $ids = $handle->getGroupIds();
00301         foreach ( $ids as $index => $id ) {
00302             if ( $id === $group->getId() ) {
00303                 unset( $ids[$index] );
00304             }
00305         }
00306 
00307         return $ids;
00308     }
00309 
00317     public static function getParentGroups( MessageGroup $targetGroup ) {
00318         $ids = self::getSharedGroups( $targetGroup );
00319         if ( $ids === array() ) {
00320             return array();
00321         }
00322 
00323         $targetId = $targetGroup->getId();
00324 
00325         /* Get the group structure. We will be using this to find which
00326          * of our candidates are top-level groups. Prefilter it to only
00327          * contain aggregate groups. */
00328         $structure = self::getGroupStructure();
00329         foreach ( $structure as $index => $group ) {
00330             if ( $group instanceof MessageGroup ) {
00331                 unset( $structure[$index] );
00332             } else {
00333                 $structure[$index] = array_shift( $group );
00334             }
00335         }
00336 
00337         /* Now that we have all related groups, use them to find all paths
00338          * from top-level groups to target group with any number of subgroups
00339          * in between. */
00340         $paths = array();
00341 
00342         /* This function recursively finds paths to the target group */
00343         $pathFinder = function ( &$paths, $group, $targetId, $prefix = '' )
00344         use ( &$pathFinder ) {
00345             if ( $group instanceof AggregateMessageGroup ) {
00349                 foreach ( $group->getGroups() as $subgroup ) {
00350                     $subId = $subgroup->getId();
00351                     if ( $subId === $targetId ) {
00352                         $paths[] = $prefix;
00353                         continue;
00354                     }
00355 
00356                     $pathFinder( $paths, $subgroup, $targetId, "$prefix|$subId" );
00357                 }
00358             }
00359         };
00360 
00361         // Iterate over the top-level groups only
00362         foreach ( $ids as $id ) {
00363             // First, find a top level groups
00364             $group = self::getGroup( $id );
00365 
00366             // Quick escape for leaf groups
00367             if ( !$group instanceof AggregateMessageGroup ) {
00368                 continue;
00369             }
00370 
00371             foreach ( $structure as $rootGroup ) {
00375                 if ( $rootGroup->getId() === $group->getId() ) {
00376                     // Yay we found a top-level group
00377                     $pathFinder( $paths, $rootGroup, $targetId, $id );
00378                     break; // No we have one or more paths appended into $paths
00379                 }
00380             }
00381         }
00382 
00383         // And finally explode the strings
00384         foreach ( $paths as $index => $pathString ) {
00385             $paths[$index] = explode( '|', $pathString );
00386         }
00387 
00388         return $paths;
00389     }
00390 
00391     private function __construct() {
00392     }
00393 
00398     public static function singleton() {
00399         static $instance;
00400         if ( !$instance instanceof self ) {
00401             $instance = new self();
00402         }
00403 
00404         return $instance;
00405     }
00406 
00411     public function getGroups() {
00412         self::init();
00413         // Expand groups to objects
00414         foreach ( self::$groups as $id => $mixed ) {
00415             if ( !is_object( $mixed ) ) {
00416                 self::$groups[$id] = call_user_func( $mixed, $id );
00417             }
00418         }
00419 
00420         return self::$groups;
00421     }
00422 
00431     public static function getGroupsById( array $ids, $skipMeta = false ) {
00432         $groups = array();
00433         foreach ( $ids as $id ) {
00434             $group = self::getGroup( $id );
00435 
00436             if ( $group !== null ) {
00437                 if ( $skipMeta && $group->isMeta() ) {
00438                     continue;
00439                 } else {
00440                     $groups[$id] = $group;
00441                 }
00442             } else {
00443                 wfDebug( __METHOD__ . ": Invalid message group id: $id\n" );
00444             }
00445         }
00446 
00447         return $groups;
00448     }
00449 
00458     public static function expandWildcards( $ids ) {
00459         $all = array();
00460 
00461         $matcher = new StringMatcher( '', (array)$ids );
00462         foreach ( self::getAllGroups() as $id => $_ ) {
00463             if ( $matcher->match( $id ) ) {
00464                 $all[] = $id;
00465             }
00466         }
00467 
00468         return $all;
00469     }
00470 
00475     public static function getDynamicGroups() {
00476         return array(
00477             '!recent' => 'RecentMessageGroup',
00478             '!additions' => 'RecentAdditionsMessageGroup',
00479             '!sandbox' => 'SandboxMessageGroup',
00480         );
00481     }
00482 
00489     public static function getGroupsByType( $type ) {
00490         wfProfileIn( __METHOD__ );
00491         $groups = self::getAllGroups();
00492         foreach ( $groups as $id => $group ) {
00493             if ( !$group instanceof $type ) {
00494                 unset( $groups[$id] );
00495             }
00496         }
00497         wfProfileOut( __METHOD__ );
00498 
00499         return $groups;
00500     }
00501 
00511     public static function getGroupStructure() {
00512         $groups = self::getAllGroups();
00513         wfProfileIn( __METHOD__ );
00514 
00515         // Determine the top level groups of the tree
00516         $tree = $groups;
00520         foreach ( $groups as $id => $o ) {
00521             if ( !$o->exists() ) {
00522                 unset( $groups[$id], $tree[$id] );
00523                 continue;
00524             }
00525 
00526             if ( $o instanceof AggregateMessageGroup ) {
00530                 foreach ( $o->getGroups() as $sid => $so ) {
00531                     unset( $tree[$sid] );
00532                 }
00533             }
00534         }
00535 
00536         // Work around php bug: https://bugs.php.net/bug.php?id=50688
00537         // Triggered by ApiQueryMessageGroups for example
00538         wfSuppressWarnings();
00539         usort( $tree, array( __CLASS__, 'groupLabelSort' ) );
00540         wfRestoreWarnings();
00541 
00542         /* Now we have two things left in $tree array:
00543          * - solitaries: top-level non-aggregate message groups
00544          * - top-level aggregate message groups */
00545         foreach ( $tree as $index => $group ) {
00546             if ( $group instanceof AggregateMessageGroup ) {
00547                 $tree[$index] = self::subGroups( $group );
00548             }
00549         }
00550 
00551         /* Essentially we are done now. Cyclic groups can cause part of the
00552          * groups not be included at all, because they have all unset each
00553          * other in the first loop. So now we check if there are groups left
00554          * over. */
00555         $used = array();
00556         // Hack to allow passing by reference
00557         array_walk_recursive( $tree, array( __CLASS__, 'collectGroupIds' ), array( &$used ) );
00558         $unused = array_diff( array_keys( $groups ), array_keys( $used ) );
00559         if ( count( $unused ) ) {
00560             foreach ( $unused as $index => $id ) {
00561                 if ( !$groups[$id] instanceof AggregateMessageGroup ) {
00562                     unset( $unused[$index] );
00563                 }
00564             }
00565 
00566             // Only list the aggregate groups, other groups cannot cause cycles
00567             $participants = implode( ', ', $unused );
00568             throw new MWException( "Found cyclic aggregate message groups: $participants" );
00569         }
00570 
00571         wfProfileOut( __METHOD__ );
00572 
00573         return $tree;
00574     }
00575 
00577     public static function collectGroupIds( $value, $key, $used ) {
00578         $used[0][$value->getId()] = true;
00579     }
00580 
00582     public static function groupLabelSort( $a, $b ) {
00583         $al = $a->getLabel();
00584         $bl = $b->getLabel();
00585 
00586         return strcasecmp( $al, $bl );
00587     }
00588 
00598     public static function subGroups( AggregateMessageGroup $parent ) {
00599         static $recursionGuard = array();
00600 
00601         $pid = $parent->getId();
00602         if ( isset( $recursionGuard[$pid] ) ) {
00603             $tid = $pid;
00604             $path = array( $tid );
00605             do {
00606                 $tid = $recursionGuard[$tid];
00607                 $path[] = $tid;
00608                 // Until we have gone full cycle
00609             } while ( $tid !== $pid );
00610             $path = implode( ' > ', $path );
00611             throw new MWException( "Found cyclic aggregate message groups: $path" );
00612         }
00613 
00614         // We don't care about the ids.
00615         $tree = array_values( $parent->getGroups() );
00616         usort( $tree, array( __CLASS__, 'groupLabelSort' ) );
00617         // Expand aggregate groups (if any left) after sorting to form a tree
00618         foreach ( $tree as $index => $group ) {
00619             if ( $group instanceof AggregateMessageGroup ) {
00620                 $sid = $group->getId();
00621                 $recursionGuard[$pid] = $sid;
00622                 $tree[$index] = self::subGroups( $group );
00623                 unset( $recursionGuard[$pid] );
00624             }
00625         }
00626 
00627         // Parent group must be first item in the array
00628         array_unshift( $tree, $parent );
00629 
00630         return $tree;
00631     }
00632 
00639     public static function haveSingleSourceLanguage( array $groups ) {
00640         $languages = array();
00641 
00642         foreach ( $groups as $group ) {
00643             $language = $group->getSourceLanguage();
00644             if ( !in_array( $language, $languages ) ) {
00645                 $languages[] = $language;
00646             }
00647         }
00648 
00649         if ( count( $languages ) === 1 ) {
00650             return $languages[0];
00651         }
00652 
00653         return '';
00654     }
00655 
00661     protected static function getAggregateGroups() {
00662         $dbw = wfGetDB( DB_MASTER );
00663         $tables = array( 'translate_metadata' );
00664         $fields = array( 'tmd_group', 'tmd_value' );
00665         $conds = array( 'tmd_key' => 'subgroups' );
00666         $res = $dbw->select( $tables, $fields, $conds, __METHOD__ );
00667 
00668         $groups = array();
00669         foreach ( $res as $row ) {
00670             $id = $row->tmd_group;
00671 
00672             $conf = array();
00673             $conf['BASIC'] = array(
00674                 'id' => $id,
00675                 'label' => TranslateMetadata::get( $id, 'name' ),
00676                 'description' => TranslateMetadata::get( $id, 'description' ),
00677                 'meta' => 1,
00678                 'class' => 'AggregateMessageGroup',
00679                 'namespace' => NS_TRANSLATIONS,
00680             );
00681             $conf['GROUPS'] = TranslateMetadata::getSubgroups( $id );
00682             $group = MessageGroupBase::factory( $conf );
00683 
00684             $groups[$id] = $group;
00685         }
00686 
00687         return $groups;
00688     }
00689 
00698     public static function isTranslatableMessage( MessageHandle $handle ) {
00699         static $cache = array();
00700 
00701         if ( !$handle->isValid() ) {
00702             return false;
00703         }
00704 
00705         $group = $handle->getGroup();
00706         $groupId = $group->getId();
00707         $language = $handle->getCode();
00708         $cacheKey = "$groupId:$language";
00709 
00710         if ( !isset( $cache[$cacheKey] ) ) {
00711             $allowed = true;
00712             $discouraged = false;
00713 
00714             $whitelist = $group->getTranslatableLanguages();
00715             if ( is_array( $whitelist ) && !isset( $whitelist[$language] ) ) {
00716                 $allowed = false;
00717             }
00718 
00719             if ( self::getPriority( $group ) === 'discouraged' ) {
00720                 $discouraged = true;
00721             } else {
00722                 $priorityLanguages = TranslateMetadata::get( $groupId, 'prioritylangs' );
00723                 if ( $priorityLanguages ) {
00724                     $map = array_flip( explode( ',', $priorityLanguages ) );
00725                     if ( !isset( $map[$language] ) ) {
00726                         $discouraged = true;
00727                     }
00728                 }
00729             }
00730 
00731             $cache[$cacheKey] = array(
00732                 'relevant' => $allowed && !$discouraged,
00733                 'tags' => array(),
00734             );
00735 
00736             $groupTags = $group->getTags();
00737             foreach ( array( 'ignored', 'optional' ) as $tag ) {
00738                 if ( isset( $groupTags[$tag] ) ) {
00739                     foreach ( $groupTags[$tag] as $key ) {
00740                         // TODO: ucfirst should not be here
00741                         $cache[$cacheKey]['tags'][ucfirst( $key )] = true;
00742                     }
00743                 }
00744             }
00745         }
00746 
00747         return $cache[$cacheKey]['relevant'] &&
00748             !isset( $cache[$cacheKey]['tags'][ucfirst( $handle->getKey() )] );
00749     }
00750 }
Generated on Tue Oct 29 00:00:24 2013 for MediaWiki Translate Extension by  doxygen 1.6.3