<?
define("WS_ADMIN",true);
require_once( "../common/WSInit.php" );
require_once( "WikiAccess.php" );
define('USAGE_LIMIT', 30);
define('NOTIFICATION_LIMIT', 100);
function getEntryTSV( $row, $usage ) {
$s= '';
$s.= $row->id . "\t";
$s.= $row->timestamp . "\t";
$s.= $row->namespace . "\t";
$s.= $row->title . "\t";
$s.= $row->user . "\t";
$s.= $row->type . "\t";
$s.= $row->action . "\t";
$s.= $row->comment . "\t";
$s.= $row->oldrev . "\t";
$s.= $row->newrev . "\t";
if (is_array($usage)) {
if ($row->namespace == NS_TEMPLATE ) {
$u= '';
foreach ($usage as $img => $pgs) {
if ($u!='') $u.= '|';
$u.= $img;
$u.= '[';
$u.= implode('#',$pgs);
$u.= ']';
}
$s.= "$u\t";
}
else {
$s.= implode('|',$usage) . "\t";
}
}
$s.= "\n";
return $s;
}
function escapeComment( $comment ) {
//$comment= str_replace('://','://',$comment); #break urls - avoid triggering captcha protection
//$comment= str_replace('</nowiki>','</nowiki>',$comment);
//$comment= '<nowiki>' . $comment . '</nowiki>';
$comment= wfEscapeWikiText($comment);
$comment= str_replace(array('}}'), array('}}'), $comment); //NOTE: don't close surounding template scope
return $comment;
}
function getEntryWikitext( $row, $usage ) {
global $wiki, $cwiki;
global $template, $since, $rotating;
global $actionstats;
$span= true;
$fulltitle= $row->title;
if ($row->namespace) {
$ns= $cwiki->getNsText($row->namespace);
if ($ns) $fulltitle= $ns . ':' . $fulltitle;
}
/*
* {{Benutzer:TickerEntry
|page=Image:Castle_of_Laeken.JPG
|image=Castle_of_Laeken.JPG
|user=Ben2
|type=replaced
|action=replacedOwn|subject=
|action2=|subject2=
|comment=<nowiki>Rétablissement de la version précédente</nowiki>
|diff=
|minor=1|template=|notice=
|usage=[[Schloss_Laeken|Schloss Laeken]], [[Benutzer:Athenchen/Fotos|Benutzer:Athenchen/Fotos]]
}} <!-- ticker-id: x --> <!-- ticker-timestamp: x -->
*/
$s= '* ';
$s.= '{{'."$template\n"; #ARG! DO NOT use "\{\{$" in PHP5, and not "{{$" in PHP4 :(
#$s.= wfTimestamp(TS_DB, $row->timestamp) . ', ';
if (!$rotating) {
#TODO: make this configurable!
$s.= "|status=\n"; #placeholder for custom use - maybe strike through, highlite, etc
$s.= "|editor=\n";
$s.= "|notice=\n";
}
$s.= "|page=$fulltitle\n";
if ($row->namespace == NS_IMAGE) $s.= "|image={$row->title}\n";
$s.= "|user={$row->user}\n";
$s.= "|type={$row->type}\n";
if ($row->type == 'tagged') $prefix= 'commons:Template:';
else if ($row->type == 'recat') $prefix= 'commons:Category:';
else $prefix= 'commons:';
if ( ($row->type == 'tagged' || $row->type == 'recat' || $row->type == 'redir')
&& preg_match('/^\s*([+*] [^\s]*)?\s*([-!] [^\s]*)?\s*$/',$row->action,$m)) {
if (isset($m[1]) && $m[1]) {
$kind= ( $m[1]{0} == '*' ) ? 'Good' : 'Bad';
$t= getWikiLinks(explode('|',substr($m[1],2)),$prefix);
$s.= "|action=added$kind|subject=$t\n";
$num= '2';
if (!isset($actionstats["added$kind"])) $actionstats["added$kind"]= 0;
$actionstats["added$kind"]+= 1;
}
else {
$num= '';
}
if (isset($m[2]) && $m[2]) {
$kind= ( $m[2]{0} == '!' ) ? 'Good' : 'Bad';
$t= getWikiLinks(explode('|',substr($m[2],2)),$prefix);
$s.= "|action$num=removed$kind|subject$num=$t\n";
if (!isset($actionstats["removed$kind"])) $actionstats["removed$kind"]= 0;
$actionstats["removed$kind"]+= 1;
if (!$num) $num= '2';
else $num= '3';
}
#hack!...
if ($num=='') $s.= "|action=\n";
if ($num=='' || $num=='2') $s.= "|action2=\n";
}
else if ($row->action) {
$s.= "|action={$row->action}\n";
$s.= "|action2=\n"; #hack...
if (!isset($actionstats[$row->action])) $actionstats[$row->action]= 0;
$actionstats[$row->action]+= 1;
}
#FIXME: missing diffs
if ($row->oldrev && $row->newrev) $s.= '|diff=title='.urlencodeTitle($fulltitle).'&diff='.$row->newrev.'&oldid='.$row->oldrev."\n";
if ($row->comment!==NULL && $row->comment!=='') $s.= '|comment=' . escapeComment($row->comment) . "\n";
if ($row->type == 'notify') $s.= "|notice=1\n";
if ($row->action == 'replacedOwn' || $row->action == 'deletedRev' /*|| $row->action == 'restored'*/) $s.= "|minor=1\n";
if ($row->namespace == NS_TEMPLATE) $s.= "|template=1\n";
if ($row->namespace == NS_IMAGE && is_array($usage)) $s.= '|usage='.getUsageWikitext($usage)."\n";
if ( $since && $since > $row->timestamp ) $s.= '|latecomer=1'."\n";
$s.= '}}';
$s.= '<!-- ticker-id: '.$row->id.' --> ';
$s.= '<!-- timestamp: '.$row->timestamp.' --> ';
$s.= "\n";
if ($row->namespace == NS_TEMPLATE && is_array($usage)) {
$s.= getTemplateUsageWikitext($row->title,$usage);
}
return $s;
}
function getUsageWikitext($usage) {
global $wiki;
if (!$usage) return '';
$txt= getWikiLinks($usage, '', NULL, true);
#FIXME: template/translate!
if ( sizeof($usage) >= USAGE_LIMIT ) $txt.= ' and more. ';
return $txt;
}
function getTemplateUsageWikitext($tmpl, $usage) {
global $wiki, $cwiki, $template;
if (!$usage) return '';
$txt= '';
foreach ($usage as $img => $pgs) {
if (!$pgs) continue;
#print "USAGE: ";
#print_r($img);
#print_r($pgs);
$txt.= "** {{"."$template\n";
$txt.= "|page=Image:$img\n";
$txt.= "|image=$img\n";
$txt.= "|usage=".getWikiLinks($pgs, '', USAGE_LIMIT, true)."\n";
$txt.= "|sub=1\n";
$txt.= "}}\n";
}
#FIXME: template/translate!
if ( sizeof($usage) >= USAGE_LIMIT ) $txt.= "** [{$cwiki->baseURL}?title=Special:Whatlinkshere&target=Template:".urlencodeTitle($tmpl)." and more].\n";
return $txt;
}
function getWikiLinks($pages, $prefix = '', $limit = NULL, $highlite = false) {
global $wiki;
if (!$pages) return '';
$c= 0;
$txt= '';
foreach ($pages as $u) {
if ($txt!=='') $txt.= ', ';
$u= $wiki->asTitle($u);
if ($prefix) $lnk= $prefix.$u.'|'.$u;
else $lnk= $u;
$lnk= '[['.$lnk.']]';
if ($highlite && !preg_match('/:[^\s_]/',$u)) $lnk= "'''$lnk'''"; #HACK: rough namespace detection.
$txt.= $lnk;
$c+= 1;
if ($limit && $c>=$limit) break;
}
return $txt;
}
function getImageUsage( &$record, $doNotify ) {
global $wiki;
$img= $record->title;
if ( @$record->page_namespaces && @$record->page_titles ) {
#print "--> using very fast checkusage for {$record->title}: {$record->page_titles}!\n";
$p= getUsagePages($img, $record, $record->page_namespaces, $record->page_titles, $doNotify);
return $p;
}
wsfLog("checking usage of image $img on {$wiki->domain}",LL_VERBOSE);
if (@$record->pages) {
$p= explode('|',$record->pages);
$p= '(' . implode(', ', $p) . ')'; #no quoting/escaping needed, $record->pages comes from the DB and contains page-ids.
$sql= 'SELECT page_title, page_namespace
FROM page
WHERE page_id IN '.$p.'
ORDER BY page_namespace, page_title
LIMIT '.USAGE_LIMIT;
#print "--> using fast checkusage for {$record->title}: {$record->pages}!\n";
$res= $wiki->wikiDB->query($sql, 'getImageUsage#ids');
}
else {
$sql= 'SELECT page_title, page_namespace
FROM imagelinks
JOIN page ON il_from = page_id
LEFT JOIN image ON img_name = il_to
WHERE il_to = '.$wiki->wikiDB->addQuotes($img).'
AND img_name IS NULL
ORDER BY page_namespace, page_title
LIMIT '.USAGE_LIMIT;
$res= $wiki->wikiDB->query($sql, 'getImageUsage#imagelinks');
}
$record->page_namespaces= array();
$record->page_titles= array();
while ($row= $wiki->wikiDB->fetchRow($res)) {
$record->page_namespaces[]= $row['page_namespace'];
$record->page_titles[]= $row['page_title'];
}
$wiki->wikiDB->freeResult($res);
$p= getUsagePages($img, $record, $record->page_namespaces, $record->page_titles, $doNotify);
return $p;
}
function getTemplateUsage( &$record, $doNotify ) {
global $cwiki, $wiki;
$templ= $record->title;
wsfLog("checking usage of template $templ on {$wiki->domain}",LL_VERBOSE);
$filterJoin= '';
$filterWhere= '';
if ($wiki->domain != 'commons.wikimedia.org') {
$filterJoin= 'LEFT JOIN image ON img_name = il_to';
$filterWhere= 'AND img_name IS NULL';
}
$sql= 'SELECT I.page_title as image,
GROUP_CONCAT(P.page_namespace SEPARATOR "|") as namespaces,
GROUP_CONCAT(P.page_title SEPARATOR "|") as titles
FROM '.$cwiki->dbname.'.page as I
JOIN '.$cwiki->dbname.'.templatelinks ON tl_from = I.page_id
JOIN imagelinks ON il_to = I.page_title
JOIN page as P ON il_from = P.page_id
'.$filterJoin.'
WHERE tl_namespace = '.NS_TEMPLATE.'
AND tl_title = '.$wiki->wikiDB->addQuotes($templ).'
AND I.page_namespace = '.NS_IMAGE.'
'.$filterWhere.'
GROUP BY I.page_title
LIMIT '.USAGE_LIMIT;
$usage= array();
$res= $wiki->wikiDB->query($sql, 'getTemplateUsage');
while ($row= $wiki->wikiDB->fetchRow($res)) {
$p= getUsagePages($row['image'], $record, $row['namespaces'], $row['titles'], $doNotify);
$usage[$row['image']]= $p;
}
$wiki->wikiDB->freeResult($res);
return $usage;
}
function getUsagePages( $image, &$record, $namespaces, $titles, $doNotify ) {
global $wiki, $notify, $notifications, $notifyFrom;
if (!is_array($namespaces)) $namespaces= explode('|',$namespaces);
if (!is_array($titles)) $titles= explode('|',$titles);
$count= min(sizeof($namespaces), sizeof($titles)); #NOTE: may mismatch if GROUP_CONCAT limit is hit (1024 bytes per default)
$p= array();
for ($i=0; $i<$count; $i+= 1) {
$ns= (int)trim($namespaces[$i]);
$t= $titles[$i];
$pg= $ns.':'.$titles[$i];
if ($ns) {
$n= $wiki->getNsText($ns);
if ($n) {
$t= "$n:$t";
if ($ns==NS_IMAGE || $ns==NS_CATEGORY) $t= ":$t";
}
else {
$t.= "{{ns:$ns}}:$t";
}
}
if ($pg && $record->id > $notifyFrom && $doNotify
&& ($ns % 2 == 0) && $notify
&& (!is_array($notify) || in_array($ns, $notify))) {
if (!isset($notifications[$pg])) $notifications[$pg]= array();
$notifications[$pg][$image][]= &$record;
}
$p[]= $t;
}
return $p;
}
/*
function listTagged($limit) {
global $wiki, $watchTags, $ignoreTags;
$where= '';
if ($watchTags) $where.= ' AND LOWER(tl_title) rlike '.$wiki->wikiDB->addQuotes(strtolower(makeSqlRegExp($watchTags)));
if ($ignoreTags) $where.= ' AND LOWER(tl_title) not rlike '.$wiki->wikiDB->addQuotes(strtolower(makeSqlRegExp($ignoreTags)));
$sql= 'SELECT page_title AS rc_title, GROUP_CONCAT(tl_title SEPARATOR "|") as tags FROM page JOIN templatelinks on page_id = tl_from
WHERE tl_namespace = '.NS_TEMPLATE.' AND page_namespace = '.NS_IMAGE.'
'.$where.'
GROUP BY page_id
';
if ($limit) $sql.= ' LIMIT '.(int)$limit;
wsfLog("querying templatelinks table...",LL_VERBOSE);
print "*** $sql ***\n";
$res= $wiki->wikiDB->query($sql, 'CommonsTicker');
wsfLog("query complete, analyzing.",LL_VERBOSE);
$now= wfTimestamp(TS_MW);
while ($row = $wiki->wikiDB->fetchObject($res)) {
#print "{$row->rc_title}: {$row->tags}\n";
$a= explode('|',$row->tags);
$a= array_filter($a, 'isWatchedTag');
if (!$a) continue;
$row->rc_action= '= '.implode('|',$a);
$row->rc_action_type= 'status';
$row->rc_timestamp= $now;
$row->rc_user_text= NULL;
$row->rc_comment= NULL;
$row->rc_last_oldid= NULL;
$row->rc_this_oldid= NULL;
putEntry($row);
}
$wiki->wikiDB->freeResult($res);
}
*/
/*
function readStatus($file) {
$data= file($file);
if (!$file) return false;
$status= array();
foreach ($data as $line) {
$line= trim($line);
if ($line==='') continue;
if ($line[0]==='#') continue;
$a= preg_split('!\s*[:=\s]\s*!', $line, 2);
if (!$a) continue;
$k= $a[0];
$v= isset($a[1]) ? $a[1] : true;
$status[$k]= $v;
}
return $status;
}
function updateStatus($file, $status) {
$s= '';
$status['_timestamp_']= wfTimestamp(TS_MW);
foreach ($status as $k => $v) {
if ($v===true) $v= 1;
else if ($v===false) $v= 0;
else if ($v===NULL) continue;
$s.= $k.' = '.$v."\n";
}
if ($s==='') return true;
if ( file_exists($file) ) {
$bak= "$file.bak";
if ( file_exists($bak) ) unlink($bak);
$ok= rename($file, $bak);
if (!$ok) {
trigger_error("faield to create backup $bak of $file", E_USER_WARNING);
return false;
}
}
else $bak= NULL;
$ok= file_put_contents($file, $s);
if (!$ok) {
trigger_error("faield to write status to $file", E_USER_WARNING);
if ($bak) {
print "RESTORING $bak\n";
$ok= rename($bak, $file);
if (!$ok) {
trigger_error("faield to restore backup $bak to $file", E_USER_ERROR);
return false;
}
}
return false;
}
return true;
} */
function postNotifications( $bot, $notifications ) {
global $notifyTemplate, $notifyPage, $wiki, $file;
if ($file) print "posting notification for ".sizeof($notifications)." pages\n";
#look for redirects
$talks= array();
$where= '';
$c= 0;
foreach ($notifications as $page => $entries) {
if ($c > NOTIFICATION_LIMIT) break;
$c+= 1;
if ($where!=='') $where.= ' OR ';
list( $ns, $t )= explode(':', $page);
$where.= " ( page_namespace = ".($ns + 1)." AND page_title = ".$wiki->wikiDB->addQuotes($t)." ) ";
}
$sql= "SELECT * FROM page JOIN pagelinks ON pl_from = page_id WHERE page_is_redirect = 1 AND ( $where )";
$res= $wiki->wikiDB->query( $sql, 'postNotifications#findRedirects');
while ($row = $wiki->wikiDB->fetchRow($res) ) {
$p= ($row['page_namespace'] - 1) . ':' . $row['page_title'];
$t= $row['pl_title'];
$n= $wiki->getNsText($row['pl_namespace']);
if ($n) $t= "$n:$t";
$talks[$p]= $t;
print "NOTE: talk page of $p is a redirect pointing to $t\n";
}
if (!$talks) print "no redirects to consider\n";
$wiki->wikiDB->freeResult($res);
$c= 0;
foreach ($notifications as $page => $entries) {
if ($c > NOTIFICATION_LIMIT) {
if ($file) print "NOTE: posted the maximum of ".NOTIFICATION_LIMIT." notifications, ignoring remaining ".(sizeof($notifications) - NOTIFICATION_LIMIT)." pages\n";
break;
}
if (isset($talks[$page])) $talk= $talks[$page];
else {
list( $ns, $t )= explode(':', $page);
$n= $wiki->getNsText($ns);
if ($n) $p= "$n:$t";
else $p= $t;
$talk= $wiki->getTalkPage( $p );
}
$s= "";
if ($notifyPage) $s.= "\n= [[:$talk]] =\n"; #for debugging
$comment= "Commons activity notification (".sizeof($entries)." images): "; #LOCALIZE!!!!!!!!!!!
if ($notifyPage) $comment= "[[:$talk]]: $comment"; #for debugging
$s.= '{{subst:'.$notifyTemplate.'}}'."\n"; #TODO: timestamp?!
$s.= "<div class=\"tickerList\">\n";
foreach ($entries as $img => $events) {
if ( sizeof($events) == 1 && $events[0]->namespace == NS_IMAGE ) {
$s.= getEntryWikitext($events[0], NULL);
}
else {
$s.= "* [[:Image:$img]]:\n"; #LOCALIZE?!
foreach ($events as $rec) {
$s.= '*'.getEntryWikitext($rec, NULL);
}
}
$comment.= "$img ";
#$comment.= "[[:Image:$img]] "; #LOCALIZE?! #FIXME: those links are red!!!!!!!!!!!!!!!!!!!!!
}
$s.="</div>";
$s.= "\n-- ~~~~\n";
$p= $notifyPage ? $notifyPage : $talk;
if ($file) print "posting notification to $p\n";
$ok= $bot->edit($p, $s, $comment); #TODO: create & use insertion marker ?
if (!$ok) {
echo "ERROR: failed to post notifications to $p\n";
}
$c+= 1;
}
}
function postFile( $bot, $page, $file, $comment ) {
global $insertBefore, $insertAfter, $insertReplace, $separator;
$text= file_get_contents($file);
if ($text===false || $text===NULL) {
echo "ERROR: failed to read $file for posting\n";
return false;
}
$text= trim($text);
if ($text==='') {
$ok= true; #nothingto post
}
elseif (trim(preg_replace('/\s*<!--.*?-->\s*/s', '', $text))==='') {
$ok= true; #nothingto post (detect comments-only)
}
else {
$now= wfTimestamp(TS_MW);
$text= "<!-- ticker update start ($now) -->\n$text\n<!-- ticker update end ($now) -->\n";
//FIXME: fresh timestamps break detection of redundant edits.
$ok= $bot->edit($page, $text, $comment, $insertBefore, $insertAfter, $insertReplace, $separator, true);
}
if ($ok) renameFileToBackup( $file );
return $ok;
}
function postNotice( $bot, $page, $text, $comment ) {
$ok= $bot->edit($page, $text, $comment);
return $ok;
}
function postPendingFiles( $bot, $page, $name, $until = NULL ) {
$ff= getPendingFiles($name);
if (!$ff) return true; #ugh...
foreach ($ff as $f) {
if ($until && basename($f)==basename($until)) break;
print "NOTE: reposting $f!\n";
$comment= "reposting previously failed update: ".basename($f);
$ok= postFile($bot, $page, $f, $comment);
if (!$ok) {
echo "ERROR: failed to repost $f. Aborting postPendingFiles\n";
return false;
}
}
return true;
}
function renameFileToBackup($name) {
if (!preg_match('!\.pending$!',$name) && file_exists("$name.pending")) $name= "$name.pending";
$tgt= preg_replace('!\.pending$!','',$name) . '.bak';
return rename($name, $tgt); #TODO: make atomic, rollback, etc...
}
function getPendingFiles($name) {
$ff= glob("$name.*"); #note: we rely on the fact that the files come back sorted by name
if (!$ff) return $ff;
$pending= array();
foreach ($ff as $f) {
if (!preg_match('!\.\d+\.pending$!',$f)) continue;
$pending[]= $f;
}
return $pending;
}
function purgeOldFiles($name, $keep = 3) {
$ff= glob("$name.*"); #note: we rely on the fact that the files come back sorted by name
if (!$ff) return false;
$ff= array_reverse($ff);
$c= 0;
foreach ($ff as $f) {
if (!preg_match('!\.\d+\.bak$!',$f)) continue;
if ($keep>0) $keep-= 1;
else unlink($f);
}
return $c;
}
function getNewestFile($name) {
$ff= glob("$name.*"); #note: we rely on the fact that the files come back sorted by name
if (!$ff) return false;
$ff= array_reverse($ff);
$pending= array();
foreach ($ff as $f) {
if (!preg_match('!\.\d+\.(pending|bak)$!',$f)) continue;
return $f;
}
return false;
}
function getNewFilename($name, $ext = 'pending') {
if ($ext) $ext= ".$ext";
return $name . '.' . wfTimestamp(TS_MW) . $ext;
}
function ignoreIfOlder($text, $timestamp, $limit) {
if ($timestamp < $limit) return "";
else return $text;
}
function stripOldEntries($text, $limit) {
global $template;
$text = preg_replace('/<!-- ticker update start \((\d+)\) -->'.$template.'<!-- ticker update end \(\1\) -->/sie', 'ignoreIfOlder("\0", "\1" ,"'.$from.'")', $text);
$text = preg_replace('/(^==.*?==\s*$)+(^==.*?==\s*$)/im', '\2', $text); //strip empty headings. (risky!)
return $text;
}
$nominor= isset($options['nominor']);
$incremental= isset($options['inc']) || isset($options['incremental']) || isset($options['update']);
$rotating= isset($options['rot']) || isset($options['rotating']);
$append= @$options['append'];
$since= @$options['since'];
$limit= @$options['limit'];
$from= @$options['from'];
$strip= @$options['strip'];
$notify= @$options['notify'];
$post= @$options['post'];
$noupdate= @$options['noupdate'];
$insertBefore= @$options['insert-before'];
$insertAfter= @$options['insert-after'];
$insertReplace= @$options['insert-replace'];
$separator= @$options['separator'];
$reverse= @$options['reverse'];
$wikitext= isset($options['wiki']);
if ($post===1 || $post===true) $post= "Project:CommonsTicker";
if ($notify===1 || $notify===true) $notify= "0";
if (!is_null($notify) && $notify!==false) {
$notify= preg_split('@[,/|;]@',$notify);
}
$notifyPage= @$options['notify-page']; #for debugging. forces notification messages to a specific page
$notifyTemplate= @$options['notify-template'];
if ($notify && !$notifyTemplate) $notifyTemplate= 'TickerNotification';
if ($rotating && !$since) $since= '-3d';
if ($since && preg_match('/^-(\d+)([mhdw])?$/',strtolower($since),$m)) {
$ofs= (int)$m[1];
if ($m[2]) {
if ($m[2]=='m') $ofs *= 60;
else if ($m[2]=='h') $ofs *= 60 * 60;
else if ($m[2]=='d') $ofs *= 60 * 60 * 24;
else if ($m[2]=='w') $ofs *= 60 * 60 * 24 * 7;
}
$t= time() - $ofs;
$since= wfTimestamp(TS_MW, $t);
}
if (isset($options['ll'])) $wsgLogLevel= $options['ll'];
$file= @$options['file'];
if ($file === '-') $file= NULL;
if (!$args) {
echo "USAGE: CommonsTicker [options] <wiki>\n";
exit(1);
}
if ($noupdate) $cwiki= NULL;
else {
$cwiki= WikiAccess::newInstance('commons.wikimedia.org');
if (!$cwiki || !$cwiki->domain) die("failed to initialized WikiAccess for commons.wikimedia.org\n");
}
if ($noupdate || ($args[0]!='-' && $args[0]!='commons' && $args[0]!='commons.wikimedia.org' )) {
$dom= $args[0];
$wiki= WikiAccess::newInstance($dom);
if (!$wiki || !$wiki->domain) die("no such wiki: $dom\n");
}
else {
$wiki=& $cwiki;
}
$wiki->initTranslations();
if ($notify) {
for ($i=0; $i<sizeof($notify); $i+=1) {
$n= $notify[$i];
if (is_numeric($n) || is_int($n)) $notify[$i]= (int)$n;
else if (strtolower($n)=='main' || strtolower($n)=='article') $notify[$i]= 0;
else $notify[$i]= $wiki->getNsIndex($n);
}
}
$oldtime= NULL; #timestamp of the last record processed - used for inserting per-day headings
if (!$file && ($incremental || $rotating || $post)) {
$file= str_replace('.','-',$wiki->domain);
if ($wikitext) $file.= '.ticker.wiki';
else $file.= '.ticker.tsv';
}
if ($incremental) print "incremental mode\n";
if ($rotating) print "rotating mode\n";
if ($file) {
if ($incremental || $rotating) $newfile= getNewFilename($file);
else $newfile;
print "output file is $newfile\n";
}
else $newfile= false;
$notifyFrom= 0;
if ($incremental || ($noupdate && $post) || ($rotating && $notify)) {
$latest= getNewestFile($file);
if ($latest && file_exists($latest)) {
print "found latest file $latest, parsing for update...\n";
$maxid= 0;
$maxtime= 0;
$h= fopen( $latest, 'r');
while ( true ) {
$s= fgets($h);
if ($s===false) break; #error
if ($s==='') break; #EOF
if ( preg_match('/<!-- ticker-id: (\d+) -->/',$s,$m) ) $maxid= max($maxid, $m[1]);
if ( preg_match('/<!-- timestamp: (\d+) -->/',$s,$m) ) $maxtime= max($maxtime, $m[1]);
if ( preg_match('/<!-- max ticker-id: (\d+) -->/',$s,$m) ) $maxid= $m[1];
if ( preg_match('/<!-- max timestamp: (\d+) -->/',$s,$m) ) $maxtime= $m[1];
}
fclose($h);
if ($maxid) print "max id in latest file: $maxid\n";
if ($maxtime) print "max timstamp in latest file: $maxtime\n";
if ($maxtime) $oldtime= $maxtime;
if ($notify) {
$notifyFrom= $maxid;
}
if ($incremental || ($noupdate && $post)) {
if (!$since && !$from) {
if (!$from && $maxid) $from= $maxid + 1;
if (!$since && $maxtime) $since= $maxtime;
}
else {
$oldtime= $since;
}
}
}
}
$notifications= array();
if (!$noupdate) {
$init= isset($options['init']) || isset($options['tagged']);
$ignoreTags= @$options['ignore']; #TODO....
$template= @$options['template'];
$warningTemplate= @$options['warning-template'];
if (!$template) $template= 'TickerEntry';
if (!$warningTemplate) $warningTemplate= 'TickerWarning';
$tickerDBName= isset($options['db']);
if ($tickerDBName===1 || $tickerDBName===true) $tickerDBName= $wsgAuxWikiDB;
if (!$tickerDBName) $tickerDBName= $wsgAuxWikiDB;
if (preg_match('!//.+?/([_a-zA-Z0-9]+)!', $tickerDBName, $m)) { //UGLY HACK - we only want the db name, not a full connection string.
$tickerDBName = $m[1];
}
#TODO: allow incremental TSV output?...
if ($incremental && !$wikitext) {
print "incremental mode, forcing wikitext output\n";
$wikitext= true;
}
if ($rotating && !$wikitext) {
print "rotating mode, forcing wikitext output\n";
$wikitext= true;
}
if ($post && !$wikitext) {
print "post requested, forcing wikitext output\n";
$wikitext= true;
}
if (!$incremental && !$rotating && !$append && $newfile && file_exists($newfile)) {
$append= true;
print "output file exists, new data will be appended\n";
}
#since one day ago per default
if (!$since && !$from) {
$since= wfTimestamp(TS_MW, time() - 60*60*24);
}
if ($since) $since= wfTimestamp(TS_MW,$since);
if ($notify && (@$options['force-notify'] || !$notifyFrom) && $from) $notifyFrom= $from;
if ($file) {
if ($since && !$from) print "using entries since $since\n"; #NOTE: if $from is set, ignore $since - this way, "latecommers" are included.
if ($from) print "using entries from id $from\n";
if ($notifyFrom && $notify) print "notifying for entries from id $notifyFrom\n";
if ($notify) print "notifying for namespaces ".implode(',', $notify)."\n";
if ($limit) print "using no more than $limit entries\n";
if ($post) print "will post to $post\n";
}
$db=& $wiki->wikiDB;
#if (!$db) die("failed to connect to ticker DB\n");
$where= '';
if ($since && !$from) $where= ' timestamp >= '.$since; #NOTE: if $from is set, ignore $since - this way, "latecommers" are included.
if ($from) $where= ' id >= '.$from;
if ($where) $where= ' AND '.$where;
if ($limit) $lim= " LIMIT $limit";
else $lim= "";
#NOTE: filter by imagelinks table early, avoid useles runs of CheckUsage.
$sql= 'SELECT * FROM (
SELECT commons_ticker.* ' . /*, NULL as pages */ '
FROM '.$tickerDBName.'.commons_ticker
WHERE namespace != ' . NS_IMAGE . ' ' . $where . '
UNION
SELECT commons_ticker.* ' . /*, GROUP_CONCAT(il_from SEPARATOR "|") as pages */ '
FROM '.$tickerDBName.'.commons_ticker
JOIN '.$wiki->dbname.'.imagelinks on il_to = title and namespace = ' . NS_IMAGE . ' ' . $where . '
GROUP BY id
) as X ';
#NOTE: collect page title/namespaces right away.
/*$sql= 'SELECT * FROM (
SELECT commons_ticker.*, NULL as page_namespaces, NULL as page_titles
FROM '.$tickerDBName.'.commons_ticker
WHERE namespace != ' . NS_IMAGE . ' ' . $where . '
UNION
SELECT commons_ticker.*, GROUP_CONCAT(page_namespace SEPARATOR "|") as page_namespaces,
GROUP_CONCAT(page_title SEPARATOR "|") as page_titles
FROM '.$tickerDBName.'.commons_ticker
JOIN '.$wiki->dbname.'.imagelinks on il_to = title and namespace = ' . NS_IMAGE . ' ' . $where . '
JOIN '.$wiki->dbname.'.page ON page_id = il_from
LEFT JOIN '.$wiki->dbname.'.image on title = img_name
WHERE img_name IS NULL
GROUP BY id
) as X ';
*/
$sql.= ' ORDER BY timestamp ';
if ($reverse) $sql.= ' DESC ';
else $sql.= ' ASC ';
if ($limit) $sql.= $lim;
if ($file) print "running database query...\n";
$res= $db->query($sql, "CommonsTicker");
if ($file) print "analyzing for ".$wiki->domain.", " . ($append ? 'appending' : 'writing' ) . " output to $newfile...\n";
if ($file && $nominor) print "skipping minor changes (deletedRev, replacedOwn, restored)\n";
$out= fopen($newfile ? $newfile : "php://stdout", $append ? 'a' : 'w');
$actionstats= array();
$maxid= 0;
$maxtime= 0;
$c= 0;
$i= 0;
$prevtime= NULL;
if ($oldtime && !$reverse) $prevtime= $oldtime;
while ($row= $db->fetchObject($res)) {
$i+= 1;
if ($nominor) {
if ( $row->type === 'deleted' && $row->action === 'deletedRev' ) continue;
if ( $row->type === 'replaced' && $row->action === 'replacedOwn' ) continue;
#if ( $row->type === 'restored' && $row->action === 'restored' ) continue;
}
$doNotify= true;
if ( $row->type === 'deleted' && $row->action === 'deletedRev' ) $doNotify= false;
if ( $row->type === 'replaced' /* && $row->action === 'replacedOwn' */) $doNotify= false;
#if ( $row->type === 'restored' /* && $row->action === 'restored' */) $doNotify= false;
if ( $row->type === 'tagged' || $row->type === 'redir' || $row->type === 'recat' ) {
if (preg_match('/[-*] /', $row->action)) $doNotify= false;
}
#FIXME: notify SINCE - separate for rotating mode !! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
if ($row->type != 'notify' && $row->namespace == NS_IMAGE && $wiki->domain!='commons.wikimedia.org') {
$row->usage= getImageUsage($row, $doNotify); #TODO: make batches of ~100 images to check...
if (!$row->usage) {
#print " - skipping unused image {$row->title}\n";
continue;
}
}
else if ($row->type != 'notify' && $row->namespace == NS_TEMPLATE) {
$row->usage= getTemplateUsage($row, $doNotify);
if (!$row->usage) {
#print " - skipping unused template {$row->title}\n";
continue;
}
}
else {
$row->usage= NULL;
}
if ($wikitext) {
#detect new day (based on UTC timestamp)
if ($prevtime!=NULL && substr($prevtime,0,8) != substr($row->timestamp,0,8) && (!$since || $row->timestamp>=$since)) {
$d= $wiki->formatDate( $row->timestamp, false );
fwrite($out, "\n== $d <!-- timestamp: {$row->timestamp} --> ==\n" );
}
}
if ($wikitext) $s= getEntryWikitext($row, $row->usage);
else $s= getEntryTSV($row, $row->usage);
fwrite($out, $s);
$maxid= max($maxid, $row->id);
$maxtime= max($maxtime, $row->timestamp);
$prevtime= $row->timestamp;
$c+= 1;
unset($row); #detach reference
}
#HACK: record id/timestamp to continue with, even if nothing was found
if ($c==0 && $incremental && $wikitext) {
$maxid= $from -1;
$maxtime= $since;
}
if ($wikitext) fwrite($out, "<!-- max ticker-id: $maxid --> <!-- max timestamp: $maxtime -->\n");
#detect new day (based on UTC timestamp)
if ($prevtime && $since && $reverse && substr($prevtime,0,8) != substr($since,0,8) && (!$since || $prevtime>=$since)) {
$d= $wiki->formatDate( $since, false );
fwrite($out, "\n== $d <!-- timestamp: {$since} --> ==\n" );
}
fclose($out);
$db->freeResult($res);
#$db->close();
if ($file) print "analyzed $i entries, reported $c as relevant\n";
}
if (($post && $file) || ($notify && $notifications)) {
require_once('WikiBot.php');
$bot= new WikiBot($wiki);
if (@$options['trace-http']) $bot->traceLog= $options['trace-http'];
if (@$options['dry']) $bot->debugDir= $options['dry'];
$ok= $bot->assertLogin();
if (!$ok) {
print "WARNING: failed to log in! Can't post updates!\n";
$bot= NULL;
}
}
if ($post && $file && $bot) {
if ($incremental) {
$ok= postPendingFiles( $bot, $post, $file, $newfile );
}
if ($incremental && !$ok) {
print "failed to post pending files, skipping current file.\n";
}
else if ($c==0) {
print "nothing to post.\n";
renameFileToBackup( $newfile );
}
else {
$comment= '';
foreach ($actionstats as $action => $n) {
#TODO: localize!
if ($comment!=='') $comment.= ', ';
$comment.= $n.' '.$action;
}
if ($since) $comment= "since $since: $comment";
if (@$options['fail-post']) {
print "NOTE: simulating failed updated (--fail-post)\n";
$ok= false;
}
else {
$ok= postFile( $bot, $post, $newfile, $comment );
}
if (!$ok && !$incremental) renameFileToBackup( $newfile );
if (!$ok && ($incremental || $rotating)) {
if ($incremental) print "WARNING: page not posted; will retry posting it on next run!\n";
else if ($rotating) print "WARNING: page not posted; will fix itself with next run.\n";
else print "WARNING: page not posted; please post manually!\n";
#FIXME: localize! Read message from file!
$cmnt= 'CommonsTicker failed to post update!';
$msg=
'== CommonsTicker failed to post update: '.basename($newfile).' ==
CommonsTicker was unable to post the latest update to the ticker page. This may happen
if there is a temporary problem with the servers or the network, or if the page has
grown verry big.
If you use "append" mode, please keep the page reasonably short, perhaps
by moving old entries that still need attention to a separate page, which could be included
in the main ticker page. Note that CommonsTicker will re-try to post the update on
the next run.
If you use "replace" mode, please consider requesting a shorter interval (less days) to watch.
CommonsTicker will not try to re-post the update, since it will replace the entire page
on the next run anyway.
(this is an automated message) -- ~~~~
';
if (@$options['local-contact']) postNotice( $bot, $wiki->getTalkPage( $options['local-contact'] ), $msg, $cmnt);
postNotice( $bot, $wiki->getTalkPage( $post ), $msg, $cmnt);
}
}
}
if ($notify && $notifications && $bot) {
if (!isset($bot)) {
require_once('WikiBot.php');
$bot= new WikiBot($wiki);
}
postNotifications( $bot, $notifications );
}
$wiki->close();
if ($cwiki) $cwiki->close();
if ($file) purgeOldFiles($file);
if ($file) print "done.\n\n";
?>CommonsTicker.php
application/x-php, 37490 bytes (load raw)

