r47242 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r47241‎ | r47242 | r47243 >
Date:23:51, 13 February 2009
Author:werdna
Status:deferred
Tags:
Comment:
Break out LqtPages and LqtModel into their own pages/ and classes/ directory, with ONE FILE PER CLASS.
Modified paths:
  • /trunk/extensions/LiquidThreads/LiquidThreads.php (modified) (history)
  • /trunk/extensions/LiquidThreads/LqtModel.php (deleted) (history)
  • /trunk/extensions/LiquidThreads/classes (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtDate.php (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtHistoricalThread.php (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtNewMessages.php (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtPost.php (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtQueryGroup.php (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtThread.php (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtThreadHistoryIterator.php (added) (history)
  • /trunk/extensions/LiquidThreads/classes/LqtThreads.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages (added) (history)
  • /trunk/extensions/LiquidThreads/pages/IndividualThreadHistoryView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/NewUserMessagesView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/SpecialDeleteThread.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/SpecialMoveThread.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/SpecialNewMessages.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/SummaryPageView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/TalkpageArchiveView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/TalkpageHeaderView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/TalkpageView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/ThreadDiffView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/ThreadHistoricalRevisionView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/ThreadHistoryListingView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/ThreadPermalinkView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/ThreadProtectionFormView.php (added) (history)
  • /trunk/extensions/LiquidThreads/pages/ThreadWatchView.php (added) (history)

Diff [purge]

Index: trunk/extensions/LiquidThreads/LqtModel.php
@@ -1,1210 +0,0 @@
2 -<?php
3 -
4 -/**
5 -* @package MediaWiki
6 -* @subpackage LiquidThreads
7 -* @author David McCabe <davemccabe@gmail.com>
8 -* @licence GPL2
9 -*/
10 -
11 -if( !defined( 'MEDIAWIKI' ) ) {
12 - echo( "This file is an extension to the MediaWiki software and cannot be used standalone.\n" );
13 - die( -1 );
14 -}
15 -
16 -require_once('Article.php');
17 -
18 -$wgHooks['TitleGetRestrictions'][] = 'Thread::getRestrictionsForTitle';
19 -
20 -// TODO if we're gonna have a Date class we should really do it.
21 -class Date {
22 - public $year, $month, $day, $hour, $minute, $second;
23 -
24 - // ex. "20070530033751"
25 - function __construct( $text ) {
26 - if ( !strlen( $text ) == 14 || !ctype_digit($text) ) {
27 - $this->isValid = false;
28 - return null;
29 - }
30 - $this->year = intval( substr( $text, 0, 4 ) );
31 - $this->month = intval( substr( $text, 4, 2 ) );
32 - $this->day = intval( substr( $text, 6, 2 ) );
33 - $this->hour = intval( substr( $text, 8, 2 ) );
34 - $this->minute = intval( substr( $text, 10, 2 ) );
35 - $this->second = intval( substr( $text, 12, 2 ) );
36 - }
37 - function lastMonth() {
38 - return $this->moved('-1 month');
39 - }
40 - function nextMonth() {
41 - return $this->moved('+1 month');
42 - }
43 - function moved($str) {
44 - // Try to set local timezone to attempt to avoid E_STRICT errors.
45 - global $wgLocaltimezone;
46 - if ( isset( $wgLocaltimezone ) ) {
47 - $oldtz = getenv( "TZ" );
48 - putenv( "TZ=$wgLocaltimezone" );
49 - }
50 - // Suppress warnings for installations without a set timezone.
51 - wfSuppressWarnings();
52 - // Make the date string.
53 - $date = date( 'YmdHis', strtotime( $this->text() . ' ' . $str ) );
54 - // Restore warnings, date no loner an issue.
55 - wfRestoreWarnings();
56 - // Generate the date object,
57 - $date = new Date( $date );
58 - // Restore the old timezone if needed.
59 - if ( isset( $wgLocaltimezone ) ) {
60 - putenv( "TZ=$oldtz" );
61 - }
62 - // Return the generated date object.
63 - return $date;
64 - }
65 - /* function monthString() {
66 - return sprintf( '%04d%02d', $this->year, $this->month );
67 - }
68 - */
69 - static function monthString($text) {
70 - return substr($text, 0, 6);
71 - }
72 -
73 - function delta( $o ) {
74 - $t = clone $this;
75 - $els = array('year', 'month', 'day', 'hour', 'minute', 'second');
76 - $deltas = array();
77 - foreach ($els as $e) {$deltas[$e] = $t->$e - $o->$e;
78 - $t->$e += $t->$e - $o->$e;
79 - }
80 -
81 - // format in style of date().
82 - $result = "";
83 - foreach( $deltas as $name => $val ) {
84 - $result .= "$val $name ";
85 - }
86 - return $result;
87 - }
88 - static function beginningOfMonth($yyyymm) { return $yyyymm . '00000000'; }
89 - static function endOfMonth($yyyymm) { return $yyyymm . '31235959'; }
90 - function text() {
91 - return sprintf( '%04d%02d%02d%02d%02d%02d', $this->year, $this->month, $this->day,
92 - $this->hour, $this->minute, $this->second );
93 - }
94 - static function now() {
95 - return new Date(wfTimestampNow());
96 - }
97 - function nDaysAgo($n) {
98 - return $this->moved("-$n days");
99 - }
100 - function midnight() {
101 - $d = clone $this;
102 - $d->hour = $d->minute = $d->second = 0;
103 - return $d;
104 - }
105 - function isBefore($d) {
106 - foreach(array('year', 'month', 'day', 'hour', 'minute', 'second') as $part) {
107 - if ( $this->$part < $d->$part ) return true;
108 - if ( $this->$part > $d->$part ) return false;
109 - }
110 - return true; // exactly the same time; arguable.
111 - }
112 -}
113 -
114 -
115 -// TODO get rid of this class. sheesh.
116 -class Post extends Article {
117 - /**
118 - * Return the User object representing the author of the first revision
119 - * (or null, if the database is screwed up).
120 - */
121 - function originalAuthor() {
122 - $dbr =& wfGetDB( DB_SLAVE );
123 -
124 - $line = $dbr->selectRow( array('revision', 'page'), 'rev_user_text',
125 - array('rev_page = page_id',
126 - 'page_id' => $this->getID()),
127 - __METHOD__,
128 - array('ORDER BY'=> 'rev_timestamp',
129 - 'LIMIT' => '1') );
130 - if ( $line )
131 - return User::newFromName($line->rev_user_text, false);
132 - else
133 - return null;
134 - }
135 -}
136 -
137 -
138 -class ThreadHistoryIterator extends ArrayIterator {
139 -
140 - function __construct($thread, $limit, $offset) {
141 - $this->thread = $thread;
142 - $this->limit = $limit;
143 - $this->offset = $offset;
144 - $this->loadRows();
145 - }
146 -
147 - private function loadRows() {
148 - if( $this->offset == 0 ) {
149 - $this->append( $this->thread );
150 - $this->limit -= 1;
151 - } else {
152 - $this->offset -= 1;
153 - }
154 -
155 - $dbr =& wfGetDB( DB_SLAVE );
156 - $res = $dbr->select(
157 - 'historical_thread',
158 - 'hthread_contents, hthread_revision',
159 - array('hthread_id' => $this->thread->id()),
160 - __METHOD__,
161 - array('ORDER BY' => 'hthread_revision DESC',
162 - 'LIMIT' => $this->limit,
163 - 'OFFSET' => $this->offset));
164 - while($l = $dbr->fetchObject($res)) {
165 - $this->append( HistoricalThread::fromTextRepresentation($l->hthread_contents) );
166 - }
167 - }
168 -}
169 -
170 -
171 -class HistoricalThread extends Thread {
172 - function __construct($t) {
173 - /* SCHEMA changes must be reflected here. */
174 - $this->rootId = $t->rootId;
175 - $this->rootRevision = $t->rootRevision;
176 - $this->articleId = $t->articleId;
177 - $this->summaryId = $t->summaryId;
178 - $this->articleNamespace = $t->articleNamespace;
179 - $this->articleTitle = $t->articleTitle;
180 - $this->modified = $t->modified;
181 - $this->created = $t->created;
182 - $this->ancestorId = $t->ancestorId;
183 - $this->parentId = $t->parentId;
184 - $this->id = $t->id;
185 - $this->revisionNumber = $t->revisionNumber;
186 - $this->changeType = $t->changeType;
187 - $this->changeObject = $t->changeObject;
188 - $this->changeComment = $t->changeComment;
189 - $this->changeUser = $t->changeUser;
190 - $this->changeUserText = $t->changeUserText;
191 - $this->editedness = $t->editedness;
192 -
193 - $this->replies = array();
194 - foreach ($t->replies as $r) {
195 - $this->replies[] = new HistoricalThread($r);
196 - }
197 - }
198 - static function textRepresentation($t) {
199 - $ht = new HistoricalThread($t);
200 - return serialize($ht);
201 - }
202 - static function fromTextRepresentation($r) {
203 - return unserialize($r);
204 - }
205 - static function create( $t, $change_type, $change_object ) {
206 - $tmt = $t->topmostThread();
207 - $contents = HistoricalThread::textRepresentation($tmt);
208 - $dbr =& wfGetDB( DB_MASTER );
209 - $res = $dbr->insert( 'historical_thread', array(
210 - 'hthread_id'=>$tmt->id(),
211 - 'hthread_revision'=>$tmt->revisionNumber(),
212 - 'hthread_contents'=>$contents,
213 - 'hthread_change_type'=>$tmt->changeType(),
214 - 'hthread_change_object'=>$tmt->changeObject() ? $tmt->changeObject()->id() : null),
215 - __METHOD__ );
216 - }
217 - static function withIdAtRevision( $id, $rev ) {
218 - $dbr =& wfGetDB( DB_SLAVE );
219 - $line = $dbr->selectRow(
220 - 'historical_thread',
221 - 'hthread_contents',
222 - array('hthread_id' => $id, 'hthread_revision' => $rev),
223 - __METHOD__);
224 - if ( $line )
225 - return HistoricalThread::fromTextRepresentation($line->hthread_contents);
226 - else
227 - return null;
228 - }
229 - function isHistorical() {
230 - return true;
231 - }
232 -}
233 -
234 -
235 -class Thread {
236 - /* SCHEMA changes must be reflected here. */
237 -
238 - /* ID references to other objects that are loaded on demand: */
239 - protected $rootId;
240 - protected $articleId;
241 - protected $summaryId;
242 - protected $ancestorId;
243 - protected $parentId;
244 -
245 - /* Actual objects loaded on demand from the above when accessors are called: */
246 - protected $root;
247 - protected $article;
248 - protected $summary;
249 - protected $superthread;
250 -
251 - /* Subject page of the talkpage we're attached to: */
252 - protected $articleNamespace;
253 - protected $articleTitle;
254 -
255 - /* Timestamps: */
256 - protected $modified;
257 - protected $created;
258 -
259 - protected $id;
260 - protected $revisionNumber;
261 - protected $type;
262 -
263 - /* Flag about who has edited or replied to this thread. */
264 - protected $editedness;
265 -
266 - /* Information about what changed in this revision. */
267 - protected $changeType;
268 - protected $changeObject;
269 - protected $changeComment;
270 - protected $changeUser;
271 - protected $changeUserText;
272 -
273 - /* Only used by $double to be saved into a historical thread. */
274 - protected $rootRevision;
275 -
276 - /* Copy of $this made when first loaded from database, to store the data
277 - we will write to the history if a new revision is commited. */
278 - protected $double;
279 -
280 - protected $replies;
281 -
282 - function isHistorical() {
283 - return false;
284 - }
285 -
286 - function revisionNumber() {
287 - return $this->revisionNumber;
288 - }
289 -
290 - function atRevision($r) {
291 - if ( $r == $this->revisionNumber() )
292 - return $this;
293 - else
294 - return HistoricalThread::withIdAtRevision($this->id(), $r);
295 - }
296 -
297 - function historicalRevisions() {
298 - $dbr =& wfGetDB( DB_SLAVE );
299 - $res = $dbr->select(
300 - 'historical_thread',
301 - 'hthread_contents',
302 - array('hthread_id' => $this->id()),
303 - __METHOD__);
304 - $results = array();
305 - while($l = $dbr->fetchObject($res)) {
306 - $results[] = HistoricalThread::fromTextRepresentation($l->hthread_contents);
307 - }
308 - return $results;
309 - }
310 -/*
311 - function ancestors() {
312 - $id_clauses = array();
313 - foreach( explode('.', $this->path) as $id ) {
314 - $id_clauses[] = "thread_id = $id";
315 - }
316 - $where = implode(' OR ', $id_clauses);
317 - return Threads::where($where);
318 - }
319 -*/
320 - private function bumpRevisionsOnAncestors($change_type, $change_object, $change_reason, $timestamp) {
321 - global $wgUser; // TODO global.
322 -
323 - $this->revisionNumber += 1;
324 - $this->setChangeType($change_type);
325 - $this->setChangeObject($change_object);
326 - $this->changeComment = $change_reason;
327 - $this->changeUser = $wgUser->getID();
328 - $this->changeUserText = $wgUser->getName();
329 -
330 - if( $this->hasSuperthread() )
331 - $this->superthread()->bumpRevisionsOnAncestors($change_type, $change_object, $change_reason, $timestamp);
332 - $dbr =& wfGetDB( DB_MASTER );
333 - $res = $dbr->update( 'thread',
334 - /* SET */ array('thread_revision' => $this->revisionNumber,
335 - 'thread_change_type'=>$this->changeType,
336 - 'thread_change_object'=>$this->changeObject,
337 - 'thread_change_comment' => $this->changeComment,
338 - 'thread_change_user' => $this->changeUser,
339 - 'thread_change_user_text' => $this->changeUserText,
340 - 'thread_modified' => $timestamp),
341 - /* WHERE */ array( 'thread_id' => $this->id ),
342 - __METHOD__);
343 - }
344 -
345 - private static function setChangeOnDescendents($thread, $change_type, $change_object) {
346 - // TODO this is ludicrously inefficient.
347 - $thread->setChangeType($change_type);
348 - $thread->setChangeObject($change_object);
349 - $dbr =& wfGetDB( DB_MASTER );
350 - $res = $dbr->update( 'thread',
351 - /* SET */ array('thread_revision' => $thread->revisionNumber,
352 - 'thread_change_type'=>$thread->changeType,
353 - 'thread_change_object'=>$thread->changeObject),
354 - /* WHERE */ array( 'thread_id' => $thread->id ),
355 - __METHOD__);
356 - foreach($thread->replies() as $r)
357 - self::setChangeOnDescendents($r, $change_type, $change_object);
358 - return $thread;
359 - }
360 -
361 - function commitRevision($change_type, $change_object = null, $reason = "") {
362 - global $wgUser; // TODO global.
363 - /*
364 - $this->changeComment = $reason;
365 - $this->changeUser = $wgUser->getID();
366 - $this->changeUserText = $wgUser->getName();
367 - */
368 - // TODO open a transaction.
369 - HistoricalThread::create( $this->double, $change_type, $change_object );
370 -
371 - $this->bumpRevisionsOnAncestors($change_type, $change_object, $reason, wfTimestampNow());
372 - self::setChangeOnDescendents($this->topmostThread(), $change_type, $change_object);
373 -
374 - if( $change_type == Threads::CHANGE_REPLY_CREATED
375 - && $this->editedness == Threads::EDITED_NEVER ) {
376 - $this->editedness = Threads::EDITED_HAS_REPLY;
377 - }
378 - else if( $change_type == Threads::CHANGE_EDITED_ROOT ) {
379 - if( $wgUser->getId() == 0 || $wgUser->getId() != $this->root()->originalAuthor()->getId() ) {
380 - $this->editedness = Threads::EDITED_BY_OTHERS;
381 - } else if( $this->editedness == Threads::EDITED_HAS_REPLY ) {
382 - $this->editedness = Threads::EDITED_BY_AUTHOR;
383 - }
384 - }
385 -
386 - /* SCHEMA changes must be reflected here. */
387 -
388 - $dbr =& wfGetDB( DB_MASTER );
389 - $res = $dbr->update( 'thread',
390 - /* SET */array( 'thread_root' => $this->rootId,
391 - 'thread_ancestor' => $this->ancestorId,
392 - 'thread_parent' => $this->parentId,
393 - 'thread_type' => $this->type,
394 - 'thread_summary_page' => $this->summaryId,
395 -// 'thread_modified' => wfTimestampNow(),
396 -// 'thread_revision' => $this->revisionNumber,
397 - 'thread_article_namespace' => $this->articleNamespace,
398 - 'thread_article_title' => $this->articleTitle,
399 - 'thread_editedness' => $this->editedness,
400 -// 'thread_change_type' => $this->changeType,
401 -// 'thread_change_object' => $this->changeObject,
402 -// 'thread_change_comment' => $this->changeComment,
403 -// 'thread_change_user' => $this->changeUser,
404 -// 'thread_change_user_text' => $this->changeUserText,
405 - ),
406 - /* WHERE */ array( 'thread_id' => $this->id, ),
407 - __METHOD__);
408 -
409 - if( $change_type == Threads::CHANGE_EDITED_ROOT ) {
410 - NewMessages::writeMessageStateForUpdatedThread($this);
411 - }
412 -
413 - // RecentChange::notifyEdit( wfTimestampNow(), $this->root(), /*minor*/false, $wgUser, $summary,
414 - // $lastRevision, $this->getModified(), $bot, '', $oldsize, $newsize,
415 - // $revisionId );
416 - }
417 -
418 - function delete($reason) {
419 - $this->type = Threads::TYPE_DELETED;
420 - $this->revisionNumber += 1;
421 - $this->commitRevision(Threads::CHANGE_DELETED, $this, $reason);
422 - /* TODO: mark thread as read by all users, or we get blank thingies in New Messages. */
423 - }
424 - function undelete($reason) {
425 - $this->type = Threads::TYPE_NORMAL;
426 - $this->revisionNumber += 1;
427 - $this->commitRevision(Threads::CHANGE_UNDELETED, $this, $reason);
428 - }
429 -
430 - function moveToSubjectPage($title, $reason, $leave_trace) {
431 - $dbr =& wfGetDB( DB_MASTER );
432 -
433 - $new_articleNamespace = $title->getNamespace();
434 - $new_articleTitle = $title->getDBkey();
435 -
436 - foreach($this->replies as $r) {
437 - $res = $dbr->update( 'thread',
438 - /* SET */array(
439 - 'thread_revision' => $r->revisionNumber() + 1,
440 - 'thread_article_namespace' => $new_articleNamespace,
441 - 'thread_article_title' => $new_articleTitle),
442 - /* WHERE */ array( 'thread_id' => $r->id(), ),
443 - __METHOD__);
444 - }
445 -
446 - $this->articleNamespace = $new_articleNamespace;
447 - $this->articleTitle = $new_articleTitle;
448 - $this->revisionNumber += 1;
449 - $this->commitRevision(Threads::CHANGE_MOVED_TALKPAGE, null, $reason);
450 -
451 - if($leave_trace) {
452 - $this->leaveTrace($reason);
453 - }
454 - }
455 -
456 - function leaveTrace($reason) {
457 - /* Adapted from Title::moveToNewTitle. But now the new title exists on the old talkpage. */
458 - $dbw =& wfGetDB( DB_MASTER );
459 -
460 - $mwRedir = MagicWord::get( 'redirect' );
461 - $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $this->title()->getPrefixedText() . "]]\n";
462 - $redirectArticle = new Article( LqtView::incrementedTitle( $this->subjectWithoutIncrement(),
463 - NS_LQT_THREAD) ); ## TODO move to model.
464 - $newid = $redirectArticle->insertOn( $dbw );
465 - $redirectRevision = new Revision( array(
466 - 'page' => $newid,
467 - 'comment' => $reason,
468 - 'text' => $redirectText ) );
469 - $redirectRevision->insertOn( $dbw );
470 - $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
471 -
472 - # Log the move
473 - $log = new LogPage( 'move' );
474 - $log->addEntry( 'move', $this->double->title(), $reason, array( 1 => $this->title()->getPrefixedText()) );
475 -
476 - # Purge caches as per article creation
477 - Article::onArticleCreate( $redirectArticle->getTitle() );
478 -
479 - # Record the just-created redirect's linking to the page
480 - $dbw->insert( 'pagelinks',
481 - array(
482 - 'pl_from' => $newid,
483 - 'pl_namespace' => $redirectArticle->getTitle()->getNamespace(),
484 - 'pl_title' => $redirectArticle->getTitle()->getDBkey() ),
485 - __METHOD__ );
486 -
487 - $thread = Threads::newThread( $redirectArticle, $this->double->article(), null,
488 - Threads::TYPE_MOVED, $log);
489 -
490 - # Purge old title from squid
491 - # The new title, and links to the new title, are purged in Article::onArticleCreate()
492 -# $this-->purgeSquid();
493 - }
494 -
495 -
496 -
497 - function __construct($line, $children) {
498 - /* SCHEMA changes must be reflected here. */
499 -
500 - $this->id = $line->thread_id;
501 - $this->rootId = $line->thread_root;
502 - $this->articleNamespace = $line->thread_article_namespace;
503 - $this->articleTitle = $line->thread_article_title;
504 - $this->summaryId = $line->thread_summary_page;
505 - $this->ancestorId = $line->thread_ancestor;
506 - $this->parentId = $line->thread_parent;
507 - $this->modified = $line->thread_modified;
508 - $this->created = $line->thread_created;
509 - $this->revisionNumber = $line->thread_revision;
510 - $this->type = $line->thread_type;
511 - $this->changeType = $line->thread_change_type;
512 - $this->changeObject = $line->thread_change_object;
513 - $this->changeComment = $line->thread_change_comment;
514 - $this->changeUser = $line->thread_change_user;
515 - $this->changeUserText = $line->thread_change_user_text;
516 - $this->editedness = $line->thread_editedness;
517 -
518 - $root_title = Title::makeTitle( $line->page_namespace, $line->page_title );
519 - $this->root = new Post($root_title);
520 - $this->root->loadPageData($line);
521 - $this->rootRevision = $this->root->mLatest;
522 - }
523 -
524 - function initWithReplies( $children ) {
525 -
526 - $this->replies = $children;
527 -
528 - $this->double = clone $this;
529 - }
530 -
531 - function __clone() {
532 - // Cloning does not normally create a new array (but the clone keyword doesn't
533 - // work on arrays -- go figure).
534 -
535 - // Update: this doesn't work for some reason, but why do we update the replies array
536 - // in the first place after creating a new reply?
537 - $new_array = array();
538 - foreach( $this->replies as $r )
539 - $new_array[] = $r;
540 - $this->replies = $new_array;
541 - }
542 -
543 - /*
544 - More evidence that the way I'm doing history is totally screwed.
545 - These methods do not alter the childrens' superthread field. All they do
546 - is make sure the latest info gets into any historicalthreads we commit.
547 - */
548 - function addReply($thread) {
549 - // TODO: question for myself to ponder: We don't want the latest info in the
550 - // historical thread, duh. Why were we doing this?
551 -// $this->replies[] = $thread;
552 - }
553 - function removeReplyWithId($id) {
554 - $target = null;
555 - foreach($this->replies as $k=>$r) {
556 - if ($r->id() == $id) {
557 - $target = $k; break;
558 - }
559 - }
560 - if ($target) {
561 - unset($this->replies[$target]);
562 - return true;
563 - } else {
564 - return false;
565 - }
566 - }
567 - function replies() {
568 - return $this->replies;
569 - }
570 -
571 - function setSuperthread($thread) {
572 - $this->parentId = $thread->id();
573 - $this->ancestorId = $thread->ancestorId();
574 - }
575 -
576 - function superthread() {
577 - if( !$this->hasSuperthread() ) {
578 - return null;
579 - } else {
580 - return Threads::withId( $this->parentId );
581 - }
582 - }
583 -
584 - function hasSuperthread() {
585 - return $this->parentId != null;
586 - }
587 -
588 - function topmostThread() {
589 - // In further evidence that the history mechanism is fragile,
590 - // if we always use Threads::withId instead of returning $this,
591 - // the historical revision is not incremented and we get a
592 - // duplicate key.
593 - if( $this->ancestorId == $this->id )
594 - return $this;
595 - else
596 - return Threads::withId( $this->ancestorId );
597 - }
598 -
599 - function isTopmostThread() {
600 - return $this->ancestorId == $this->id;
601 - }
602 -
603 - function setArticle($a) {
604 - $this->articleId = $a->getID();
605 - $this->articleNamespace = $a->getTitle()->getNamespace();
606 - $this->articleTitle = $a->getTitle()->getDBkey();
607 - $this->touch();
608 - }
609 -
610 - function article() {
611 - if ( $this->article ) return $this->article;
612 - $title = Title::newFromID($this->articleId);
613 - if($title) {
614 - $a = new Article($title);
615 - }
616 - if (isset($a) && $a->exists()) {
617 - return $a;
618 - } else {
619 - return new Article( Title::makeTitle($this->articleNamespace, $this->articleTitle) );
620 - }
621 - }
622 -
623 - function id() {
624 - return $this->id;
625 - }
626 -
627 - function ancestorId() {
628 - return $this->ancestorId;
629 - }
630 -
631 - function root() {
632 - if ( !$this->rootId ) return null;
633 - if ( !$this->root ) $this->root = new Post( Title::newFromID( $this->rootId ),
634 - $this->rootRevision() );
635 - return $this->root;
636 - }
637 -
638 - function setRootRevision($rr) {
639 - if( (is_object($rr)) ) {
640 - $this->rootRevision = $rr->getId();
641 - } else if (is_int($rr)) {
642 - $this->rootRevision = $rr;
643 - }
644 - }
645 -
646 - function rootRevision() {
647 - return $this->rootRevision;
648 - }
649 -
650 - function editedness() {
651 - return $this->editedness;
652 - }
653 -
654 - function summary() {
655 - if ( !$this->summaryId ) return null;
656 - if ( !$this->summary ) $this->summary = new Post( Title::newFromID( $this->summaryId ) );
657 - return $this->summary;
658 - }
659 -
660 - function hasSummary() {
661 - return $this->summaryId != null;
662 - }
663 -
664 - function setSummary( $post ) {
665 - $this->summary = null;
666 - $this->summaryId = $post->getID();
667 - }
668 -
669 - function title() {
670 - return $this->root()->getTitle();
671 - }
672 -
673 - private function splitIncrementFromSubject($subject_string) {
674 - preg_match('/^(.*) \((\d+)\)$/', $subject_string, $matches);
675 - if( count($matches) != 3 )
676 - throw new MWException( __METHOD__ . ": thread subject has no increment: " . $subject_string );
677 - else
678 - return $matches;
679 - }
680 -
681 - function wikilink() {
682 - return $this->root()->getTitle()->getPrefixedText();
683 - }
684 -
685 - function subject() {
686 - return $this->root()->getTitle()->getText();
687 - }
688 -
689 - function wikilinkWithoutIncrement() {
690 - $tmp = $this->splitIncrementFromSubject($this->wikilink()); return $tmp[1];
691 - }
692 -
693 - function subjectWithoutIncrement() {
694 - $tmp = $this->splitIncrementFromSubject($this->subject()); return $tmp[1];
695 - }
696 -
697 - function increment() {
698 - $tmp = $this->splitIncrementFromSubject($this->subject()); return $tmp[2];
699 - }
700 -
701 - function hasDistinctSubject() {
702 - if( $this->hasSuperthread() ) {
703 - return $this->superthread()->subjectWithoutIncrement()
704 - != $this->subjectWithoutIncrement();
705 - } else {
706 - return true;
707 - }
708 - }
709 -
710 - function hasSubthreads() {
711 - return count($this->replies) != 0;
712 - }
713 -
714 - function subthreads() {
715 - return $this->replies;
716 - }
717 -
718 - function modified() {
719 - return $this->modified;
720 - }
721 -
722 - function created() {
723 - return $this->created;
724 - }
725 -
726 - function type() {
727 - return $this->type;
728 - }
729 -
730 - function changeType() {
731 - return $this->changeType;
732 - }
733 -
734 - private function replyWithId($id) {
735 - if( $this->id == $id ) return $this;
736 - foreach ( $this->replies as $r ) {
737 - if( $r->id() == $id ) return $r;
738 - else {
739 - $s = $r->replyWithId($id);
740 - if( $s ) return $s;
741 - }
742 - }
743 - return null;
744 - }
745 - function changeObject() {
746 - return $this->replyWithId( $this->changeObject );
747 - }
748 -
749 - function setChangeType($t) {
750 - if (in_array($t, Threads::$VALID_CHANGE_TYPES)) {
751 - $this->changeType = $t;
752 - } else {
753 - throw new MWException( __METHOD__ . ": invalid changeType $t." );
754 - }
755 - }
756 -
757 - function setChangeObject($o) {
758 - # we assume $o to be a Thread.
759 - if($o === null) {
760 - $this->changeObject = null;
761 - } else {
762 - $this->changeObject = $o->id();
763 - }
764 - }
765 -
766 - function changeUser() {
767 - if( $this->changeUser == 0 ) {
768 - return User::newFromName($this->changeUserText, false);
769 - } else {
770 - return User::newFromId($this->changeUser);
771 - }
772 - }
773 -
774 - function changeComment() {
775 - return $this->changeComment;
776 - }
777 -
778 - function redirectThread() {
779 - $rev = Revision::newFromId($this->root()->getLatest());
780 - $rtitle = Title::newFromRedirect($rev->getRawText());
781 - if( !$rtitle ) return null;
782 - $rthread = Threads::withRoot(new Article($rtitle));
783 - return $rthread;
784 - }
785 -
786 - // Called from hook in Title::isProtected.
787 - static function getRestrictionsForTitle($title, $action, &$result) {
788 - $thread = Threads::withRoot(new Post($title));
789 - if ($thread)
790 - return $thread->getRestrictions($action, $result);
791 - else
792 - return true; // not a thread; do normal protection check.
793 - }
794 -
795 - // This only makes sense when called from the hook, because it uses the hook's
796 - // default behavior to check whether this thread itself is protected, so you'll
797 - // get false negatives if you use it from some other context.
798 - function getRestrictions($action, &$result) {
799 - if( $this->hasSuperthread() ) {
800 - $parent_restrictions = $this->superthread()->root()->getTitle()->getRestrictions($action);
801 - } else {
802 - $parent_restrictions = $this->article()->getTitle()->getTalkPage()->getRestrictions($action);
803 - }
804 -
805 - // TODO this may not be the same as asking "are the parent restrictions more restrictive than
806 - // our own restrictions?", which is what we really want.
807 - if( count($parent_restrictions) == 0 ) {
808 - return true; // go to normal protection check.
809 - } else {
810 - $result = $parent_restrictions;
811 - return false;
812 - }
813 -
814 - }
815 -}
816 -
817 -
818 -/** Module of factory methods. */
819 -class Threads {
820 -
821 - const TYPE_NORMAL = 0;
822 - const TYPE_MOVED = 1;
823 - const TYPE_DELETED = 2;
824 - static $VALID_TYPES = array(self::TYPE_NORMAL, self::TYPE_MOVED, self::TYPE_DELETED);
825 -
826 - const CHANGE_NEW_THREAD = 0;
827 - const CHANGE_REPLY_CREATED = 1;
828 - const CHANGE_EDITED_ROOT = 2;
829 - const CHANGE_EDITED_SUMMARY = 3;
830 - const CHANGE_DELETED = 4;
831 - const CHANGE_UNDELETED = 5;
832 - const CHANGE_MOVED_TALKPAGE = 6;
833 - static $VALID_CHANGE_TYPES = array(self::CHANGE_EDITED_SUMMARY, self::CHANGE_EDITED_ROOT,
834 - self::CHANGE_REPLY_CREATED, self::CHANGE_NEW_THREAD, self::CHANGE_DELETED, self::CHANGE_UNDELETED,
835 - self::CHANGE_MOVED_TALKPAGE);
836 -
837 - // Possible values of Thread->editedness.
838 - const EDITED_NEVER = 0;
839 - const EDITED_HAS_REPLY = 1;
840 - const EDITED_BY_AUTHOR = 2;
841 - const EDITED_BY_OTHERS = 3;
842 -
843 - static $cache_by_root = array();
844 - static $cache_by_id = array();
845 -
846 - static function newThread( $root, $article, $superthread = null, $type = self::TYPE_NORMAL ) {
847 - // SCHEMA changes must be reflected here.
848 - // TODO: It's dumb that the commitRevision code isn't used here.
849 -
850 - $dbr =& wfGetDB( DB_MASTER );
851 -
852 - if ( !in_array($type, self::$VALID_TYPES) ) {
853 - throw new MWException(__METHOD__ . ": invalid type $type.");
854 - }
855 -
856 - if ($superthread) {
857 - $change_type = self::CHANGE_REPLY_CREATED;
858 - } else {
859 - $change_type = self::CHANGE_NEW_THREAD;
860 - }
861 -
862 - global $wgUser; // TODO global.
863 -
864 - $timestamp = wfTimestampNow();
865 -
866 - $res = $dbr->insert('thread',
867 - array('thread_root' => $root->getID(),
868 - 'thread_parent' => $superthread ? $superthread->id() : null,
869 - 'thread_article_namespace' => $article->getTitle()->getNamespace(),
870 - 'thread_article_title' => $article->getTitle()->getDBkey(),
871 - 'thread_modified' => $timestamp,
872 - 'thread_created' => $timestamp,
873 - 'thread_change_type' => $change_type,
874 - 'thread_change_comment' => "", // TODO
875 - 'thread_change_user' => $wgUser->getID(),
876 - 'thread_change_user_text' => $wgUser->getName(),
877 - 'thread_type' => $type,
878 - 'thread_editedness' => self::EDITED_NEVER),
879 - __METHOD__);
880 -
881 - $newid = $dbr->insertId();
882 -
883 - if( $superthread ) {
884 - $ancestor = $superthread->ancestorId();
885 - $change_object_clause = 'thread_change_object = ' . $newid;
886 - } else {
887 - $ancestor = $newid;
888 - $change_object_clause = 'thread_change_object = null';
889 - }
890 - $res = $dbr->update( 'thread',
891 - /* SET */ array( 'thread_ancestor' => $ancestor,
892 - $change_object_clause ),
893 - /* WHERE */ array( 'thread_id' => $newid, ),
894 - __METHOD__);
895 -
896 - // TODO we could avoid a query here.
897 - $newthread = Threads::withId($newid);
898 - if($superthread) {
899 - $superthread->addReply( $newthread );
900 - }
901 -
902 - self::createTalkpageIfNeeded($article);
903 -
904 - NewMessages::writeMessageStateForUpdatedThread($newthread);
905 -
906 - return $newthread;
907 - }
908 -
909 - /**
910 - * Create the talkpage if it doesn't exist so that links to it
911 - * will show up blue instead of red. For use upon new thread creation.
912 - */
913 - protected static function createTalkpageIfNeeded($subjectPage) {
914 - $talkpage_t = $subjectPage->getTitle()->getTalkpage();
915 - $talkpage = new Article($talkpage_t);
916 - if( ! $talkpage->exists() ) {
917 - try {
918 - wfLoadExtensionMessages( 'LiquidThreads' );
919 - $talkpage->doEdit( "", wfMsg('lqt_talkpage_autocreate_summary'), EDIT_NEW | EDIT_SUPPRESS_RC );
920 -
921 - } catch( DBQueryError $e ) {
922 - // The article already existed by now. No need to do anything.
923 - wfDebug(__METHOD__ . ": Article already existed by the time we tried to create it.");
924 - }
925 - }
926 - }
927 -
928 - static function where( $where, $options = array(), $extra_tables = array(), $joins = "" ) {
929 - global $wgDBprefix;
930 - $dbr = wfGetDB( DB_SLAVE );
931 - if ( is_array($where) ) $where = $dbr->makeList( $where, LIST_AND );
932 - if ( is_array($options) ) $options = implode(',', $options);
933 -
934 - if( is_array($extra_tables) && count($extra_tables) != 0 ) {
935 - if(!empty($wgDBprefix)) {
936 - foreach($extra_tables as $tablekey=>$extra_table)
937 - $extra_tables[$tablekey]=$wgDBprefix.$extra_table;
938 - }
939 - $tables = implode(',', $extra_tables) . ', ';
940 - } else if ( is_string( $extra_tables ) ) {
941 - $tables = $extra_tables . ', ';
942 - } else {
943 - $tables = "";
944 - }
945 -
946 -
947 - $selection_sql = <<< SQL
948 - SELECT DISTINCT thread.* FROM ($tables {$wgDBprefix}thread thread)
949 - $joins
950 - WHERE $where
951 - $options
952 -SQL;
953 - $selection_res = $dbr->query($selection_sql);
954 -
955 - $ancestor_conds = array();
956 - $selection_conds = array();
957 - while( $line = $dbr->fetchObject($selection_res) ) {
958 - $ancestor_conds[] = $line->thread_ancestor;
959 - $selection_conds[] = $line->thread_id;
960 - }
961 - if( count($selection_conds) == 0 ) {
962 - // No threads were found, so we can skip the second query.
963 - return array();
964 - } // List comprehensions, how I miss thee.
965 - $ancestor_clause = join(', ', $ancestor_conds);
966 - $selection_clause = join(', ', $selection_conds);
967 -
968 - $children_sql = <<< SQL
969 - SELECT DISTINCT thread.*, page.*,
970 - thread.thread_id IN($selection_clause) as selected
971 - FROM ({$wgDBprefix}thread thread, {$wgDBprefix}page page)
972 - WHERE thread.thread_ancestor IN($ancestor_clause)
973 - AND page.page_id = thread.thread_root
974 - $options
975 -SQL;
976 - $res = $dbr->query($children_sql);
977 -
978 - $threads = array();
979 - $top_level_threads = array();
980 - $thread_children = array();
981 -
982 - while ( $line = $dbr->fetchObject($res) ) {
983 - $new_thread = new Thread($line, null);
984 - $threads[] = $new_thread;
985 - if( $line->selected )
986 - // thread is one of those that was directly queried for.
987 - $top_level_threads[] = $new_thread;
988 - if( $line->thread_parent !== null ) {
989 - if( !array_key_exists( $line->thread_parent, $thread_children ) )
990 - $thread_children[$line->thread_parent] = array();
991 - // Can have duplicate if thread is both top_level and child of another top_level thread.
992 - if( !self::arrayContainsThreadWithId($thread_children[$line->thread_parent], $new_thread->id()) )
993 - $thread_children[$line->thread_parent][] = $new_thread;
994 - }
995 - }
996 -
997 - foreach( $threads as $thread ) {
998 - if( array_key_exists( $thread->id(), $thread_children ) ) {
999 - $thread->initWithReplies( $thread_children[$thread->id()] );
1000 - } else {
1001 - $thread->initWithReplies( array() );
1002 - }
1003 -
1004 - self::$cache_by_root[$thread->root()->getID()] = $thread;
1005 - self::$cache_by_id[$thread->id()] = $thread;
1006 - }
1007 -
1008 - return $top_level_threads;
1009 - }
1010 -
1011 - private static function databaseError( $msg ) {
1012 - // TODO tie into MW's error reporting facilities.
1013 - echo("Corrupt liquidthreads database: $msg");
1014 - die();
1015 - }
1016 -
1017 - private static function assertSingularity( $threads, $attribute, $value ) {
1018 - if( count($threads) == 0 ) { return null; }
1019 - if( count($threads) == 1 ) { return $threads[0]; }
1020 - if( count($threads) > 1 ) {
1021 - Threads::databaseError("More than one thread with $attribute = $value.");
1022 - return null;
1023 - }
1024 - }
1025 -
1026 - private static function arrayContainsThreadWithId( $a, $id ) {
1027 - // There's gotta be a nice way to express this in PHP. Anyone?
1028 - foreach($a as $t)
1029 - if($t->id() == $id)
1030 - return true;
1031 - return false;
1032 - }
1033 -
1034 - static function withRoot( $post ) {
1035 - if( $post->getTitle()->getNamespace() != NS_LQT_THREAD ) {
1036 - // No articles outside the thread namespace have threads associated with them;
1037 - // avoiding the query saves time during the TitleGetRestrictions hook.
1038 - return null;
1039 - }
1040 - if( array_key_exists( $post->getID(), self::$cache_by_root ) ) {
1041 - return self::$cache_by_root[$post->getID()];
1042 - }
1043 - $ts = Threads::where( array('thread.thread_root' => $post->getID()) );
1044 - return self::assertSingularity($ts, 'thread_root', $post->getID());
1045 - }
1046 -
1047 - static function withId( $id ) {
1048 - if( array_key_exists( $id, self::$cache_by_id ) ) {
1049 - return self::$cache_by_id[$id];
1050 - }
1051 - $ts = Threads::where( array('thread.thread_id' => $id ) );
1052 - return self::assertSingularity($ts, 'thread_id', $id);
1053 - }
1054 -
1055 - static function withSummary( $article ) {
1056 - $ts = Threads::where( array('thread.thread_summary_page' => $article->getId()));
1057 - return self::assertSingularity($ts, 'thread_summary_page', $article->getId());
1058 - }
1059 -
1060 - /**
1061 - * Horrible, horrible!
1062 - * List of months in which there are >0 threads, suitable for threadsOfArticleInMonth. */
1063 - static function monthsWhereArticleHasThreads( $article ) {
1064 - $threads = Threads::where( Threads::articleClause($article) );
1065 - $months = array();
1066 - foreach( $threads as $t ) {
1067 - $m = substr( $t->modified(), 0, 6 );
1068 - if ( !array_key_exists( $m, $months ) ) {
1069 - if (!in_array( $m, $months )) $months[] = $m;
1070 - }
1071 - }
1072 - return $months;
1073 - }
1074 -
1075 - static function articleClause($article) {
1076 - $dbr = wfGetDB(DB_SLAVE);
1077 - $q_article= $dbr->addQuotes($article->getTitle()->getDBkey());
1078 - return <<<SQL
1079 -(thread.thread_article_title = $q_article
1080 - AND thread.thread_article_namespace = {$article->getTitle()->getNamespace()})
1081 -SQL;
1082 - }
1083 -
1084 - static function topLevelClause() {
1085 - return 'thread.thread_parent is null';
1086 - }
1087 -
1088 -}
1089 -
1090 -
1091 -class QueryGroup {
1092 - protected $queries;
1093 -
1094 - function __construct() {
1095 - $this->queries = array();
1096 - }
1097 -
1098 - function addQuery( $name, $where, $options = array(), $extra_tables = array() ) {
1099 - $this->queries[$name] = array($where, $options, $extra_tables);
1100 - }
1101 -
1102 - function extendQuery( $original, $newname, $where, $options = array(), $extra_tables=array() ) {
1103 - if (!array_key_exists($original,$this->queries)) return;
1104 - $q = $this->queries[$original];
1105 - $this->queries[$newname] = array( array_merge($q[0], $where),
1106 - array_merge($q[1], $options),
1107 - array_merge($q[2], $extra_tables) );
1108 - }
1109 -
1110 - function deleteQuery( $name ) {
1111 - unset ($this->queries[$name]);
1112 - }
1113 -
1114 - function query($name) {
1115 - if ( !array_key_exists($name,$this->queries) ) return array();
1116 - list($where, $options, $extra_tables) = $this->queries[$name];
1117 - return Threads::where($where, $options, $extra_tables);
1118 - }
1119 -}
1120 -
1121 -
1122 -class NewMessages {
1123 -
1124 - static function markThreadAsUnreadByUser($thread, $user) {
1125 - self::writeUserMessageState($thread, $user, null);
1126 - }
1127 -
1128 - static function markThreadAsReadByUser($thread, $user) {
1129 - self::writeUserMessageState($thread, $user, wfTimestampNow());
1130 - }
1131 -
1132 - private static function writeUserMessageState($thread, $user, $timestamp) {
1133 - global $wgDBprefix;
1134 - if( is_object($thread) ) $thread_id = $thread->id();
1135 - else if( is_integer($thread) ) $thread_id = $thread;
1136 - else throw new MWException("writeUserMessageState expected Thread or integer but got $thread");
1137 -
1138 - if( is_object($user) ) $user_id = $user->getID();
1139 - else if( is_integer($user) ) $user_id = $user;
1140 - else throw new MWException("writeUserMessageState expected User or integer but got $user");
1141 -
1142 - if ( $timestamp === null ) $timestamp = "NULL";
1143 -
1144 - // use query() directly to pass in 'true' for don't-die-on-errors.
1145 - $dbr =& wfGetDB( DB_MASTER );
1146 - $success = $dbr->query("insert into {$wgDBprefix}user_message_state values ($user_id, $thread_id, $timestamp)",
1147 - __METHOD__, true);
1148 -
1149 - if( !$success ) {
1150 - // duplicate key; update.
1151 - $dbr->query("update {$wgDBprefix}user_message_state set ums_read_timestamp = $timestamp" .
1152 - " where ums_thread = $thread_id and ums_user = $user_id",
1153 - __METHOD__);
1154 - }
1155 - }
1156 -
1157 - /**
1158 - * Write a user_message_state for each user who is watching the thread.
1159 - * If the thread is on a user's talkpage, set that user's newtalk.
1160 - */
1161 - static function writeMessageStateForUpdatedThread($t) {
1162 - global $wgDBprefix, $wgUser;
1163 -
1164 - if( $t->article()->getTitle()->getNamespace() == NS_USER ) {
1165 - $name = $t->article()->getTitle()->getDBkey();
1166 - list($name) = split('/', $name); // subpages
1167 - $user = User::newFromName($name);
1168 - if( $user && $user->getID() != $wgUser->getID() ) {
1169 - $user->setNewtalk(true);
1170 - }
1171 - }
1172 -
1173 - $dbw =& wfGetDB( DB_MASTER );
1174 -
1175 - $talkpage_t = $t->article()->getTitle();
1176 - $root_t = $t->root()->getTitle();
1177 -
1178 - $q_talkpage_t = $dbw->addQuotes($talkpage_t->getDBkey());
1179 - $q_root_t = $dbw->addQuotes($root_t->getDBkey());
1180 -
1181 - // Select any applicable watchlist entries for the thread.
1182 - $where_clause = <<<SQL
1183 -(
1184 - (wl_namespace = {$talkpage_t->getNamespace()} and wl_title = $q_talkpage_t )
1185 -or (wl_namespace = {$root_t->getNamespace()} and wl_title = $q_root_t )
1186 -)
1187 -SQL;
1188 -
1189 - // it sucks to not have 'on duplicate key update'. first update users who already have a ums for this thread
1190 - // and who have already read it, by setting their state to unread.
1191 - $dbw->query("update {$wgDBprefix}user_message_state, {$wgDBprefix}watchlist set ums_read_timestamp = null where ums_user = wl_user and ums_thread = {$t->id()} and $where_clause");
1192 -
1193 - $dbw->query("insert ignore into {$wgDBprefix}user_message_state (ums_user, ums_thread) select user_id, {$t->id()} from {$wgDBprefix}user, {$wgDBprefix}watchlist where user_id = wl_user and $where_clause;");
1194 - }
1195 -
1196 - static function newUserMessages($user) {
1197 - global $wgDBprefix;
1198 - return Threads::where( array('ums_read_timestamp is null',
1199 - Threads::articleClause(new Article($user->getUserPage()))),
1200 - array(), array(), "left outer join {$wgDBprefix}user_message_state on ums_user is null or (ums_user = {$user->getID()} and ums_thread = thread.thread_id)" );
1201 - }
1202 -
1203 - static function watchedThreadsForUser($user) {
1204 - return Threads::where( array('ums_read_timestamp is null',
1205 - 'ums_user' => $user->getID(),
1206 - 'ums_thread = thread.thread_id',
1207 - 'NOT (' . Threads::articleClause(new Article($user->getUserPage())) . ')' ),
1208 - array(), array('user_message_state') );
1209 - }
1210 -
1211 -}
Index: trunk/extensions/LiquidThreads/LiquidThreads.php
@@ -43,34 +43,34 @@
4444 $wgHooks['SkinTemplateTabAction'][] = 'LqtDispatch::tabAction';
4545 $wgHooks['OldChangesListRecentChangesLine'][] = 'LqtDispatch::customizeOldChangesList';
4646 $wgHooks['SkinTemplateOutputPageBeforeExec'][] = 'LqtDispatch::setNewtalkHTML';
 47+$wgHooks['TitleGetRestrictions'][] = 'Thread::getRestrictionsForTitle';
4748
4849 $wgSpecialPages['DeleteThread'] = 'SpecialDeleteThread';
4950 $wgSpecialPages['MoveThread'] = 'SpecialMoveThread';
5051 $wgSpecialPages['NewMessages'] = 'SpecialNewMessages';
5152
52 -// Obtained with $ grep -ir 'class .*' *.php | perl -n -e 'if (/(\w+\.php):\s*class (\w+)/) {print "\$wgAutoloadClasses['\''$2'\''] = \$dir.'\''$1'\'';\n";}'
5353 $wgAutoloadClasses['LqtDispatch'] = $dir.'LqtBaseView.php';
5454 $wgAutoloadClasses['LqtView'] = $dir.'LqtBaseView.php';
55 -$wgAutoloadClasses['Date'] = $dir.'LqtModel.php';
56 -$wgAutoloadClasses['Post'] = $dir.'LqtModel.php';
57 -$wgAutoloadClasses['ThreadHistoryIterator'] = $dir.'LqtModel.php';
58 -$wgAutoloadClasses['HistoricalThread'] = $dir.'LqtModel.php';
59 -$wgAutoloadClasses['Thread'] = $dir.'LqtModel.php';
60 -$wgAutoloadClasses['Threads'] = $dir.'LqtModel.php';
61 -$wgAutoloadClasses['QueryGroup'] = $dir.'LqtModel.php';
62 -$wgAutoloadClasses['NewMessages'] = $dir.'LqtModel.php';
63 -$wgAutoloadClasses['TalkpageView'] = $dir.'LqtPages.php';
64 -$wgAutoloadClasses['TalkpageArchiveView'] = $dir.'LqtPages.php';
65 -$wgAutoloadClasses['ThreadPermalinkView'] = $dir.'LqtPages.php';
66 -$wgAutoloadClasses['TalkpageHeaderView'] = $dir.'LqtPages.php';
67 -$wgAutoloadClasses['IndividualThreadHistoryView'] = $dir.'LqtPages.php';
68 -$wgAutoloadClasses['ThreadDiffView'] = $dir.'LqtPages.php';
69 -$wgAutoloadClasses['ThreadWatchView'] = $dir.'LqtPages.php';
70 -$wgAutoloadClasses['ThreadProtectionFormView'] = $dir.'LqtPages.php';
71 -$wgAutoloadClasses['ThreadHistoryListingView'] = $dir.'LqtPages.php';
72 -$wgAutoloadClasses['ThreadHistoricalRevisionView'] = $dir.'LqtPages.php';
73 -$wgAutoloadClasses['SummaryPageView'] = $dir.'LqtPages.php';
74 -$wgAutoloadClasses['SpecialMoveThread'] = $dir.'LqtPages.php';
75 -$wgAutoloadClasses['SpecialDeleteThread'] = $dir.'LqtPages.php';
76 -$wgAutoloadClasses['NewUserMessagesView'] = $dir.'LqtPages.php';
77 -$wgAutoloadClasses['SpecialNewMessages'] = $dir.'LqtPages.php';
 55+$wgAutoloadClasses['Date'] = $dir.'classes/LqtDate.php';
 56+$wgAutoloadClasses['Post'] = $dir.'classes/LqtPost.php';
 57+$wgAutoloadClasses['ThreadHistoryIterator'] = $dir.'classes/LqtThreadHistoryIterator.php';
 58+$wgAutoloadClasses['HistoricalThread'] = $dir.'classes/LqtHistoricalThread.php';
 59+$wgAutoloadClasses['Thread'] = $dir.'classes/LqtThread.php';
 60+$wgAutoloadClasses['Threads'] = $dir.'classes/LqtThreads.php';
 61+$wgAutoloadClasses['QueryGroup'] = $dir.'classes/LqtQueryGroup.php';
 62+$wgAutoloadClasses['NewMessages'] = $dir.'classes/LqtNewMessages.php';
 63+$wgAutoloadClasses['TalkpageView'] = $dir.'pages/TalkpageView.php';
 64+$wgAutoloadClasses['TalkpageArchiveView'] = $dir.'pages/TalkpageArchiveView.php';
 65+$wgAutoloadClasses['ThreadPermalinkView'] = $dir.'pages/ThreadPermalinkView.php';
 66+$wgAutoloadClasses['TalkpageHeaderView'] = $dir.'pages/TalkpageHeaderView.php';
 67+$wgAutoloadClasses['IndividualThreadHistoryView'] = $dir.'pages/IndividualThreadHistoryView.php';
 68+$wgAutoloadClasses['ThreadDiffView'] = $dir.'pages/ThreadDiffView.php';
 69+$wgAutoloadClasses['ThreadWatchView'] = $dir.'pages/ThreadWatchView.php';
 70+$wgAutoloadClasses['ThreadProtectionFormView'] = $dir.'pages/ThreadProtectionFormView.php';
 71+$wgAutoloadClasses['ThreadHistoryListingView'] = $dir.'pages/ThreadHistoryListingView.php';
 72+$wgAutoloadClasses['ThreadHistoricalRevisionView'] = $dir.'pages/ThreadHistoricalRevisionView.php';
 73+$wgAutoloadClasses['SummaryPageView'] = $dir.'pages/SummaryPageView.php';
 74+$wgAutoloadClasses['SpecialMoveThread'] = $dir.'pages/SpecialMoveThread.php';
 75+$wgAutoloadClasses['SpecialDeleteThread'] = $dir.'pages/SpecialDeleteThread.php';
 76+$wgAutoloadClasses['NewUserMessagesView'] = $dir.'pages/NewUserMessagesView.php';
 77+$wgAutoloadClasses['SpecialNewMessages'] = $dir.'pages/SpecialNewMessages.php';
\ No newline at end of file
Index: trunk/extensions/LiquidThreads/classes/LqtDate.php
@@ -0,0 +1,96 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class Date {
 7+ public $year, $month, $day, $hour, $minute, $second;
 8+
 9+ // ex. "20070530033751"
 10+ function __construct( $text ) {
 11+ if ( !strlen( $text ) == 14 || !ctype_digit($text) ) {
 12+ $this->isValid = false;
 13+ return null;
 14+ }
 15+ $this->year = intval( substr( $text, 0, 4 ) );
 16+ $this->month = intval( substr( $text, 4, 2 ) );
 17+ $this->day = intval( substr( $text, 6, 2 ) );
 18+ $this->hour = intval( substr( $text, 8, 2 ) );
 19+ $this->minute = intval( substr( $text, 10, 2 ) );
 20+ $this->second = intval( substr( $text, 12, 2 ) );
 21+ }
 22+ function lastMonth() {
 23+ return $this->moved('-1 month');
 24+ }
 25+ function nextMonth() {
 26+ return $this->moved('+1 month');
 27+ }
 28+ function moved($str) {
 29+ // Try to set local timezone to attempt to avoid E_STRICT errors.
 30+ global $wgLocaltimezone;
 31+ if ( isset( $wgLocaltimezone ) ) {
 32+ $oldtz = getenv( "TZ" );
 33+ putenv( "TZ=$wgLocaltimezone" );
 34+ }
 35+ // Suppress warnings for installations without a set timezone.
 36+ wfSuppressWarnings();
 37+ // Make the date string.
 38+ $date = date( 'YmdHis', strtotime( $this->text() . ' ' . $str ) );
 39+ // Restore warnings, date no loner an issue.
 40+ wfRestoreWarnings();
 41+ // Generate the date object,
 42+ $date = new Date( $date );
 43+ // Restore the old timezone if needed.
 44+ if ( isset( $wgLocaltimezone ) ) {
 45+ putenv( "TZ=$oldtz" );
 46+ }
 47+ // Return the generated date object.
 48+ return $date;
 49+ }
 50+ /* function monthString() {
 51+ return sprintf( '%04d%02d', $this->year, $this->month );
 52+ }
 53+ */
 54+ static function monthString($text) {
 55+ return substr($text, 0, 6);
 56+ }
 57+
 58+ function delta( $o ) {
 59+ $t = clone $this;
 60+ $els = array('year', 'month', 'day', 'hour', 'minute', 'second');
 61+ $deltas = array();
 62+ foreach ($els as $e) {$deltas[$e] = $t->$e - $o->$e;
 63+ $t->$e += $t->$e - $o->$e;
 64+ }
 65+
 66+ // format in style of date().
 67+ $result = "";
 68+ foreach( $deltas as $name => $val ) {
 69+ $result .= "$val $name ";
 70+ }
 71+ return $result;
 72+ }
 73+ static function beginningOfMonth($yyyymm) { return $yyyymm . '00000000'; }
 74+ static function endOfMonth($yyyymm) { return $yyyymm . '31235959'; }
 75+ function text() {
 76+ return sprintf( '%04d%02d%02d%02d%02d%02d', $this->year, $this->month, $this->day,
 77+ $this->hour, $this->minute, $this->second );
 78+ }
 79+ static function now() {
 80+ return new Date(wfTimestampNow());
 81+ }
 82+ function nDaysAgo($n) {
 83+ return $this->moved("-$n days");
 84+ }
 85+ function midnight() {
 86+ $d = clone $this;
 87+ $d->hour = $d->minute = $d->second = 0;
 88+ return $d;
 89+ }
 90+ function isBefore($d) {
 91+ foreach(array('year', 'month', 'day', 'hour', 'minute', 'second') as $part) {
 92+ if ( $this->$part < $d->$part ) return true;
 93+ if ( $this->$part > $d->$part ) return false;
 94+ }
 95+ return true; // exactly the same time; arguable.
 96+ }
 97+}
Index: trunk/extensions/LiquidThreads/classes/LqtNewMessages.php
@@ -0,0 +1,94 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class NewMessages {
 7+
 8+ static function markThreadAsUnreadByUser($thread, $user) {
 9+ self::writeUserMessageState($thread, $user, null);
 10+ }
 11+
 12+ static function markThreadAsReadByUser($thread, $user) {
 13+ self::writeUserMessageState($thread, $user, wfTimestampNow());
 14+ }
 15+
 16+ private static function writeUserMessageState($thread, $user, $timestamp) {
 17+ global $wgDBprefix;
 18+ if( is_object($thread) ) $thread_id = $thread->id();
 19+ else if( is_integer($thread) ) $thread_id = $thread;
 20+ else throw new MWException("writeUserMessageState expected Thread or integer but got $thread");
 21+
 22+ if( is_object($user) ) $user_id = $user->getID();
 23+ else if( is_integer($user) ) $user_id = $user;
 24+ else throw new MWException("writeUserMessageState expected User or integer but got $user");
 25+
 26+ if ( $timestamp === null ) $timestamp = "NULL";
 27+
 28+ // use query() directly to pass in 'true' for don't-die-on-errors.
 29+ $dbr =& wfGetDB( DB_MASTER );
 30+ $success = $dbr->query("insert into {$wgDBprefix}user_message_state values ($user_id, $thread_id, $timestamp)",
 31+ __METHOD__, true);
 32+
 33+ if( !$success ) {
 34+ // duplicate key; update.
 35+ $dbr->query("update {$wgDBprefix}user_message_state set ums_read_timestamp = $timestamp" .
 36+ " where ums_thread = $thread_id and ums_user = $user_id",
 37+ __METHOD__);
 38+ }
 39+ }
 40+
 41+ /**
 42+ * Write a user_message_state for each user who is watching the thread.
 43+ * If the thread is on a user's talkpage, set that user's newtalk.
 44+ */
 45+ static function writeMessageStateForUpdatedThread($t) {
 46+ global $wgDBprefix, $wgUser;
 47+
 48+ if( $t->article()->getTitle()->getNamespace() == NS_USER ) {
 49+ $name = $t->article()->getTitle()->getDBkey();
 50+ list($name) = split('/', $name); // subpages
 51+ $user = User::newFromName($name);
 52+ if( $user && $user->getID() != $wgUser->getID() ) {
 53+ $user->setNewtalk(true);
 54+ }
 55+ }
 56+
 57+ $dbw =& wfGetDB( DB_MASTER );
 58+
 59+ $talkpage_t = $t->article()->getTitle();
 60+ $root_t = $t->root()->getTitle();
 61+
 62+ $q_talkpage_t = $dbw->addQuotes($talkpage_t->getDBkey());
 63+ $q_root_t = $dbw->addQuotes($root_t->getDBkey());
 64+
 65+ // Select any applicable watchlist entries for the thread.
 66+ $where_clause = <<<SQL
 67+(
 68+ (wl_namespace = {$talkpage_t->getNamespace()} and wl_title = $q_talkpage_t )
 69+or (wl_namespace = {$root_t->getNamespace()} and wl_title = $q_root_t )
 70+)
 71+SQL;
 72+
 73+ // it sucks to not have 'on duplicate key update'. first update users who already have a ums for this thread
 74+ // and who have already read it, by setting their state to unread.
 75+ $dbw->query("update {$wgDBprefix}user_message_state, {$wgDBprefix}watchlist set ums_read_timestamp = null where ums_user = wl_user and ums_thread = {$t->id()} and $where_clause");
 76+
 77+ $dbw->query("insert ignore into {$wgDBprefix}user_message_state (ums_user, ums_thread) select user_id, {$t->id()} from {$wgDBprefix}user, {$wgDBprefix}watchlist where user_id = wl_user and $where_clause;");
 78+ }
 79+
 80+ static function newUserMessages($user) {
 81+ global $wgDBprefix;
 82+ return Threads::where( array('ums_read_timestamp is null',
 83+ Threads::articleClause(new Article($user->getUserPage()))),
 84+ array(), array(), "left outer join {$wgDBprefix}user_message_state on ums_user is null or (ums_user = {$user->getID()} and ums_thread = thread.thread_id)" );
 85+ }
 86+
 87+ static function watchedThreadsForUser($user) {
 88+ return Threads::where( array('ums_read_timestamp is null',
 89+ 'ums_user' => $user->getID(),
 90+ 'ums_thread = thread.thread_id',
 91+ 'NOT (' . Threads::articleClause(new Article($user->getUserPage())) . ')' ),
 92+ array(), array('user_message_state') );
 93+ }
 94+
 95+}
Index: trunk/extensions/LiquidThreads/classes/LqtQueryGroup.php
@@ -0,0 +1,33 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class QueryGroup {
 7+ protected $queries;
 8+
 9+ function __construct() {
 10+ $this->queries = array();
 11+ }
 12+
 13+ function addQuery( $name, $where, $options = array(), $extra_tables = array() ) {
 14+ $this->queries[$name] = array($where, $options, $extra_tables);
 15+ }
 16+
 17+ function extendQuery( $original, $newname, $where, $options = array(), $extra_tables=array() ) {
 18+ if (!array_key_exists($original,$this->queries)) return;
 19+ $q = $this->queries[$original];
 20+ $this->queries[$newname] = array( array_merge($q[0], $where),
 21+ array_merge($q[1], $options),
 22+ array_merge($q[2], $extra_tables) );
 23+ }
 24+
 25+ function deleteQuery( $name ) {
 26+ unset ($this->queries[$name]);
 27+ }
 28+
 29+ function query($name) {
 30+ if ( !array_key_exists($name,$this->queries) ) return array();
 31+ list($where, $options, $extra_tables) = $this->queries[$name];
 32+ return Threads::where($where, $options, $extra_tables);
 33+ }
 34+}
Index: trunk/extensions/LiquidThreads/classes/LqtThreadHistoryIterator.php
@@ -0,0 +1,35 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class ThreadHistoryIterator extends ArrayIterator {
 7+
 8+ function __construct($thread, $limit, $offset) {
 9+ $this->thread = $thread;
 10+ $this->limit = $limit;
 11+ $this->offset = $offset;
 12+ $this->loadRows();
 13+ }
 14+
 15+ private function loadRows() {
 16+ if( $this->offset == 0 ) {
 17+ $this->append( $this->thread );
 18+ $this->limit -= 1;
 19+ } else {
 20+ $this->offset -= 1;
 21+ }
 22+
 23+ $dbr =& wfGetDB( DB_SLAVE );
 24+ $res = $dbr->select(
 25+ 'historical_thread',
 26+ 'hthread_contents, hthread_revision',
 27+ array('hthread_id' => $this->thread->id()),
 28+ __METHOD__,
 29+ array('ORDER BY' => 'hthread_revision DESC',
 30+ 'LIMIT' => $this->limit,
 31+ 'OFFSET' => $this->offset));
 32+ while($l = $dbr->fetchObject($res)) {
 33+ $this->append( HistoricalThread::fromTextRepresentation($l->hthread_contents) );
 34+ }
 35+ }
 36+}
Index: trunk/extensions/LiquidThreads/classes/LqtPost.php
@@ -0,0 +1,24 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class Post extends Article {
 7+ /**
 8+ * Return the User object representing the author of the first revision
 9+ * (or null, if the database is screwed up).
 10+ */
 11+ function originalAuthor() {
 12+ $dbr =& wfGetDB( DB_SLAVE );
 13+
 14+ $line = $dbr->selectRow( array('revision', 'page'), 'rev_user_text',
 15+ array('rev_page = page_id',
 16+ 'page_id' => $this->getID()),
 17+ __METHOD__,
 18+ array('ORDER BY'=> 'rev_timestamp',
 19+ 'LIMIT' => '1') );
 20+ if ( $line )
 21+ return User::newFromName($line->rev_user_text, false);
 22+ else
 23+ return null;
 24+ }
 25+}
Index: trunk/extensions/LiquidThreads/classes/LqtThread.php
@@ -0,0 +1,585 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class Thread {
 7+ /* SCHEMA changes must be reflected here. */
 8+
 9+ /* ID references to other objects that are loaded on demand: */
 10+ protected $rootId;
 11+ protected $articleId;
 12+ protected $summaryId;
 13+ protected $ancestorId;
 14+ protected $parentId;
 15+
 16+ /* Actual objects loaded on demand from the above when accessors are called: */
 17+ protected $root;
 18+ protected $article;
 19+ protected $summary;
 20+ protected $superthread;
 21+
 22+ /* Subject page of the talkpage we're attached to: */
 23+ protected $articleNamespace;
 24+ protected $articleTitle;
 25+
 26+ /* Timestamps: */
 27+ protected $modified;
 28+ protected $created;
 29+
 30+ protected $id;
 31+ protected $revisionNumber;
 32+ protected $type;
 33+
 34+ /* Flag about who has edited or replied to this thread. */
 35+ protected $editedness;
 36+
 37+ /* Information about what changed in this revision. */
 38+ protected $changeType;
 39+ protected $changeObject;
 40+ protected $changeComment;
 41+ protected $changeUser;
 42+ protected $changeUserText;
 43+
 44+ /* Only used by $double to be saved into a historical thread. */
 45+ protected $rootRevision;
 46+
 47+ /* Copy of $this made when first loaded from database, to store the data
 48+ we will write to the history if a new revision is commited. */
 49+ protected $double;
 50+
 51+ protected $replies;
 52+
 53+ function isHistorical() {
 54+ return false;
 55+ }
 56+
 57+ function revisionNumber() {
 58+ return $this->revisionNumber;
 59+ }
 60+
 61+ function atRevision($r) {
 62+ if ( $r == $this->revisionNumber() )
 63+ return $this;
 64+ else
 65+ return HistoricalThread::withIdAtRevision($this->id(), $r);
 66+ }
 67+
 68+ function historicalRevisions() {
 69+ $dbr =& wfGetDB( DB_SLAVE );
 70+ $res = $dbr->select(
 71+ 'historical_thread',
 72+ 'hthread_contents',
 73+ array('hthread_id' => $this->id()),
 74+ __METHOD__);
 75+ $results = array();
 76+ while($l = $dbr->fetchObject($res)) {
 77+ $results[] = HistoricalThread::fromTextRepresentation($l->hthread_contents);
 78+ }
 79+ return $results;
 80+ }
 81+/*
 82+ function ancestors() {
 83+ $id_clauses = array();
 84+ foreach( explode('.', $this->path) as $id ) {
 85+ $id_clauses[] = "thread_id = $id";
 86+ }
 87+ $where = implode(' OR ', $id_clauses);
 88+ return Threads::where($where);
 89+ }
 90+*/
 91+ private function bumpRevisionsOnAncestors($change_type, $change_object, $change_reason, $timestamp) {
 92+ global $wgUser; // TODO global.
 93+
 94+ $this->revisionNumber += 1;
 95+ $this->setChangeType($change_type);
 96+ $this->setChangeObject($change_object);
 97+ $this->changeComment = $change_reason;
 98+ $this->changeUser = $wgUser->getID();
 99+ $this->changeUserText = $wgUser->getName();
 100+
 101+ if( $this->hasSuperthread() )
 102+ $this->superthread()->bumpRevisionsOnAncestors($change_type, $change_object, $change_reason, $timestamp);
 103+ $dbr =& wfGetDB( DB_MASTER );
 104+ $res = $dbr->update( 'thread',
 105+ /* SET */ array('thread_revision' => $this->revisionNumber,
 106+ 'thread_change_type'=>$this->changeType,
 107+ 'thread_change_object'=>$this->changeObject,
 108+ 'thread_change_comment' => $this->changeComment,
 109+ 'thread_change_user' => $this->changeUser,
 110+ 'thread_change_user_text' => $this->changeUserText,
 111+ 'thread_modified' => $timestamp),
 112+ /* WHERE */ array( 'thread_id' => $this->id ),
 113+ __METHOD__);
 114+ }
 115+
 116+ private static function setChangeOnDescendents($thread, $change_type, $change_object) {
 117+ // TODO this is ludicrously inefficient.
 118+ $thread->setChangeType($change_type);
 119+ $thread->setChangeObject($change_object);
 120+ $dbr =& wfGetDB( DB_MASTER );
 121+ $res = $dbr->update( 'thread',
 122+ /* SET */ array('thread_revision' => $thread->revisionNumber,
 123+ 'thread_change_type'=>$thread->changeType,
 124+ 'thread_change_object'=>$thread->changeObject),
 125+ /* WHERE */ array( 'thread_id' => $thread->id ),
 126+ __METHOD__);
 127+ foreach($thread->replies() as $r)
 128+ self::setChangeOnDescendents($r, $change_type, $change_object);
 129+ return $thread;
 130+ }
 131+
 132+ function commitRevision($change_type, $change_object = null, $reason = "") {
 133+ global $wgUser; // TODO global.
 134+ /*
 135+ $this->changeComment = $reason;
 136+ $this->changeUser = $wgUser->getID();
 137+ $this->changeUserText = $wgUser->getName();
 138+ */
 139+ // TODO open a transaction.
 140+ HistoricalThread::create( $this->double, $change_type, $change_object );
 141+
 142+ $this->bumpRevisionsOnAncestors($change_type, $change_object, $reason, wfTimestampNow());
 143+ self::setChangeOnDescendents($this->topmostThread(), $change_type, $change_object);
 144+
 145+ if( $change_type == Threads::CHANGE_REPLY_CREATED
 146+ && $this->editedness == Threads::EDITED_NEVER ) {
 147+ $this->editedness = Threads::EDITED_HAS_REPLY;
 148+ }
 149+ else if( $change_type == Threads::CHANGE_EDITED_ROOT ) {
 150+ if( $wgUser->getId() == 0 || $wgUser->getId() != $this->root()->originalAuthor()->getId() ) {
 151+ $this->editedness = Threads::EDITED_BY_OTHERS;
 152+ } else if( $this->editedness == Threads::EDITED_HAS_REPLY ) {
 153+ $this->editedness = Threads::EDITED_BY_AUTHOR;
 154+ }
 155+ }
 156+
 157+ /* SCHEMA changes must be reflected here. */
 158+
 159+ $dbr =& wfGetDB( DB_MASTER );
 160+ $res = $dbr->update( 'thread',
 161+ /* SET */array( 'thread_root' => $this->rootId,
 162+ 'thread_ancestor' => $this->ancestorId,
 163+ 'thread_parent' => $this->parentId,
 164+ 'thread_type' => $this->type,
 165+ 'thread_summary_page' => $this->summaryId,
 166+// 'thread_modified' => wfTimestampNow(),
 167+// 'thread_revision' => $this->revisionNumber,
 168+ 'thread_article_namespace' => $this->articleNamespace,
 169+ 'thread_article_title' => $this->articleTitle,
 170+ 'thread_editedness' => $this->editedness,
 171+// 'thread_change_type' => $this->changeType,
 172+// 'thread_change_object' => $this->changeObject,
 173+// 'thread_change_comment' => $this->changeComment,
 174+// 'thread_change_user' => $this->changeUser,
 175+// 'thread_change_user_text' => $this->changeUserText,
 176+ ),
 177+ /* WHERE */ array( 'thread_id' => $this->id, ),
 178+ __METHOD__);
 179+
 180+ if( $change_type == Threads::CHANGE_EDITED_ROOT ) {
 181+ NewMessages::writeMessageStateForUpdatedThread($this);
 182+ }
 183+
 184+ // RecentChange::notifyEdit( wfTimestampNow(), $this->root(), /*minor*/false, $wgUser, $summary,
 185+ // $lastRevision, $this->getModified(), $bot, '', $oldsize, $newsize,
 186+ // $revisionId );
 187+ }
 188+
 189+ function delete($reason) {
 190+ $this->type = Threads::TYPE_DELETED;
 191+ $this->revisionNumber += 1;
 192+ $this->commitRevision(Threads::CHANGE_DELETED, $this, $reason);
 193+ /* TODO: mark thread as read by all users, or we get blank thingies in New Messages. */
 194+ }
 195+ function undelete($reason) {
 196+ $this->type = Threads::TYPE_NORMAL;
 197+ $this->revisionNumber += 1;
 198+ $this->commitRevision(Threads::CHANGE_UNDELETED, $this, $reason);
 199+ }
 200+
 201+ function moveToSubjectPage($title, $reason, $leave_trace) {
 202+ $dbr =& wfGetDB( DB_MASTER );
 203+
 204+ $new_articleNamespace = $title->getNamespace();
 205+ $new_articleTitle = $title->getDBkey();
 206+
 207+ foreach($this->replies as $r) {
 208+ $res = $dbr->update( 'thread',
 209+ /* SET */array(
 210+ 'thread_revision' => $r->revisionNumber() + 1,
 211+ 'thread_article_namespace' => $new_articleNamespace,
 212+ 'thread_article_title' => $new_articleTitle),
 213+ /* WHERE */ array( 'thread_id' => $r->id(), ),
 214+ __METHOD__);
 215+ }
 216+
 217+ $this->articleNamespace = $new_articleNamespace;
 218+ $this->articleTitle = $new_articleTitle;
 219+ $this->revisionNumber += 1;
 220+ $this->commitRevision(Threads::CHANGE_MOVED_TALKPAGE, null, $reason);
 221+
 222+ if($leave_trace) {
 223+ $this->leaveTrace($reason);
 224+ }
 225+ }
 226+
 227+ function leaveTrace($reason) {
 228+ /* Adapted from Title::moveToNewTitle. But now the new title exists on the old talkpage. */
 229+ $dbw =& wfGetDB( DB_MASTER );
 230+
 231+ $mwRedir = MagicWord::get( 'redirect' );
 232+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $this->title()->getPrefixedText() . "]]\n";
 233+ $redirectArticle = new Article( LqtView::incrementedTitle( $this->subjectWithoutIncrement(),
 234+ NS_LQT_THREAD) ); ## TODO move to model.
 235+ $newid = $redirectArticle->insertOn( $dbw );
 236+ $redirectRevision = new Revision( array(
 237+ 'page' => $newid,
 238+ 'comment' => $reason,
 239+ 'text' => $redirectText ) );
 240+ $redirectRevision->insertOn( $dbw );
 241+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
 242+
 243+ # Log the move
 244+ $log = new LogPage( 'move' );
 245+ $log->addEntry( 'move', $this->double->title(), $reason, array( 1 => $this->title()->getPrefixedText()) );
 246+
 247+ # Purge caches as per article creation
 248+ Article::onArticleCreate( $redirectArticle->getTitle() );
 249+
 250+ # Record the just-created redirect's linking to the page
 251+ $dbw->insert( 'pagelinks',
 252+ array(
 253+ 'pl_from' => $newid,
 254+ 'pl_namespace' => $redirectArticle->getTitle()->getNamespace(),
 255+ 'pl_title' => $redirectArticle->getTitle()->getDBkey() ),
 256+ __METHOD__ );
 257+
 258+ $thread = Threads::newThread( $redirectArticle, $this->double->article(), null,
 259+ Threads::TYPE_MOVED, $log);
 260+
 261+ # Purge old title from squid
 262+ # The new title, and links to the new title, are purged in Article::onArticleCreate()
 263+# $this-->purgeSquid();
 264+ }
 265+
 266+
 267+
 268+ function __construct($line, $children) {
 269+ /* SCHEMA changes must be reflected here. */
 270+
 271+ $this->id = $line->thread_id;
 272+ $this->rootId = $line->thread_root;
 273+ $this->articleNamespace = $line->thread_article_namespace;
 274+ $this->articleTitle = $line->thread_article_title;
 275+ $this->summaryId = $line->thread_summary_page;
 276+ $this->ancestorId = $line->thread_ancestor;
 277+ $this->parentId = $line->thread_parent;
 278+ $this->modified = $line->thread_modified;
 279+ $this->created = $line->thread_created;
 280+ $this->revisionNumber = $line->thread_revision;
 281+ $this->type = $line->thread_type;
 282+ $this->changeType = $line->thread_change_type;
 283+ $this->changeObject = $line->thread_change_object;
 284+ $this->changeComment = $line->thread_change_comment;
 285+ $this->changeUser = $line->thread_change_user;
 286+ $this->changeUserText = $line->thread_change_user_text;
 287+ $this->editedness = $line->thread_editedness;
 288+
 289+ $root_title = Title::makeTitle( $line->page_namespace, $line->page_title );
 290+ $this->root = new Post($root_title);
 291+ $this->root->loadPageData($line);
 292+ $this->rootRevision = $this->root->mLatest;
 293+ }
 294+
 295+ function initWithReplies( $children ) {
 296+
 297+ $this->replies = $children;
 298+
 299+ $this->double = clone $this;
 300+ }
 301+
 302+ function __clone() {
 303+ // Cloning does not normally create a new array (but the clone keyword doesn't
 304+ // work on arrays -- go figure).
 305+
 306+ // Update: this doesn't work for some reason, but why do we update the replies array
 307+ // in the first place after creating a new reply?
 308+ $new_array = array();
 309+ foreach( $this->replies as $r )
 310+ $new_array[] = $r;
 311+ $this->replies = $new_array;
 312+ }
 313+
 314+ /*
 315+ More evidence that the way I'm doing history is totally screwed.
 316+ These methods do not alter the childrens' superthread field. All they do
 317+ is make sure the latest info gets into any historicalthreads we commit.
 318+ */
 319+ function addReply($thread) {
 320+ // TODO: question for myself to ponder: We don't want the latest info in the
 321+ // historical thread, duh. Why were we doing this?
 322+// $this->replies[] = $thread;
 323+ }
 324+ function removeReplyWithId($id) {
 325+ $target = null;
 326+ foreach($this->replies as $k=>$r) {
 327+ if ($r->id() == $id) {
 328+ $target = $k; break;
 329+ }
 330+ }
 331+ if ($target) {
 332+ unset($this->replies[$target]);
 333+ return true;
 334+ } else {
 335+ return false;
 336+ }
 337+ }
 338+ function replies() {
 339+ return $this->replies;
 340+ }
 341+
 342+ function setSuperthread($thread) {
 343+ $this->parentId = $thread->id();
 344+ $this->ancestorId = $thread->ancestorId();
 345+ }
 346+
 347+ function superthread() {
 348+ if( !$this->hasSuperthread() ) {
 349+ return null;
 350+ } else {
 351+ return Threads::withId( $this->parentId );
 352+ }
 353+ }
 354+
 355+ function hasSuperthread() {
 356+ return $this->parentId != null;
 357+ }
 358+
 359+ function topmostThread() {
 360+ // In further evidence that the history mechanism is fragile,
 361+ // if we always use Threads::withId instead of returning $this,
 362+ // the historical revision is not incremented and we get a
 363+ // duplicate key.
 364+ if( $this->ancestorId == $this->id )
 365+ return $this;
 366+ else
 367+ return Threads::withId( $this->ancestorId );
 368+ }
 369+
 370+ function isTopmostThread() {
 371+ return $this->ancestorId == $this->id;
 372+ }
 373+
 374+ function setArticle($a) {
 375+ $this->articleId = $a->getID();
 376+ $this->articleNamespace = $a->getTitle()->getNamespace();
 377+ $this->articleTitle = $a->getTitle()->getDBkey();
 378+ $this->touch();
 379+ }
 380+
 381+ function article() {
 382+ if ( $this->article ) return $this->article;
 383+ $title = Title::newFromID($this->articleId);
 384+ if($title) {
 385+ $a = new Article($title);
 386+ }
 387+ if (isset($a) && $a->exists()) {
 388+ return $a;
 389+ } else {
 390+ return new Article( Title::makeTitle($this->articleNamespace, $this->articleTitle) );
 391+ }
 392+ }
 393+
 394+ function id() {
 395+ return $this->id;
 396+ }
 397+
 398+ function ancestorId() {
 399+ return $this->ancestorId;
 400+ }
 401+
 402+ function root() {
 403+ if ( !$this->rootId ) return null;
 404+ if ( !$this->root ) $this->root = new Post( Title::newFromID( $this->rootId ),
 405+ $this->rootRevision() );
 406+ return $this->root;
 407+ }
 408+
 409+ function setRootRevision($rr) {
 410+ if( (is_object($rr)) ) {
 411+ $this->rootRevision = $rr->getId();
 412+ } else if (is_int($rr)) {
 413+ $this->rootRevision = $rr;
 414+ }
 415+ }
 416+
 417+ function rootRevision() {
 418+ return $this->rootRevision;
 419+ }
 420+
 421+ function editedness() {
 422+ return $this->editedness;
 423+ }
 424+
 425+ function summary() {
 426+ if ( !$this->summaryId ) return null;
 427+ if ( !$this->summary ) $this->summary = new Post( Title::newFromID( $this->summaryId ) );
 428+ return $this->summary;
 429+ }
 430+
 431+ function hasSummary() {
 432+ return $this->summaryId != null;
 433+ }
 434+
 435+ function setSummary( $post ) {
 436+ $this->summary = null;
 437+ $this->summaryId = $post->getID();
 438+ }
 439+
 440+ function title() {
 441+ return $this->root()->getTitle();
 442+ }
 443+
 444+ private function splitIncrementFromSubject($subject_string) {
 445+ preg_match('/^(.*) \((\d+)\)$/', $subject_string, $matches);
 446+ if( count($matches) != 3 )
 447+ throw new MWException( __METHOD__ . ": thread subject has no increment: " . $subject_string );
 448+ else
 449+ return $matches;
 450+ }
 451+
 452+ function wikilink() {
 453+ return $this->root()->getTitle()->getPrefixedText();
 454+ }
 455+
 456+ function subject() {
 457+ return $this->root()->getTitle()->getText();
 458+ }
 459+
 460+ function wikilinkWithoutIncrement() {
 461+ $tmp = $this->splitIncrementFromSubject($this->wikilink()); return $tmp[1];
 462+ }
 463+
 464+ function subjectWithoutIncrement() {
 465+ $tmp = $this->splitIncrementFromSubject($this->subject()); return $tmp[1];
 466+ }
 467+
 468+ function increment() {
 469+ $tmp = $this->splitIncrementFromSubject($this->subject()); return $tmp[2];
 470+ }
 471+
 472+ function hasDistinctSubject() {
 473+ if( $this->hasSuperthread() ) {
 474+ return $this->superthread()->subjectWithoutIncrement()
 475+ != $this->subjectWithoutIncrement();
 476+ } else {
 477+ return true;
 478+ }
 479+ }
 480+
 481+ function hasSubthreads() {
 482+ return count($this->replies) != 0;
 483+ }
 484+
 485+ function subthreads() {
 486+ return $this->replies;
 487+ }
 488+
 489+ function modified() {
 490+ return $this->modified;
 491+ }
 492+
 493+ function created() {
 494+ return $this->created;
 495+ }
 496+
 497+ function type() {
 498+ return $this->type;
 499+ }
 500+
 501+ function changeType() {
 502+ return $this->changeType;
 503+ }
 504+
 505+ private function replyWithId($id) {
 506+ if( $this->id == $id ) return $this;
 507+ foreach ( $this->replies as $r ) {
 508+ if( $r->id() == $id ) return $r;
 509+ else {
 510+ $s = $r->replyWithId($id);
 511+ if( $s ) return $s;
 512+ }
 513+ }
 514+ return null;
 515+ }
 516+ function changeObject() {
 517+ return $this->replyWithId( $this->changeObject );
 518+ }
 519+
 520+ function setChangeType($t) {
 521+ if (in_array($t, Threads::$VALID_CHANGE_TYPES)) {
 522+ $this->changeType = $t;
 523+ } else {
 524+ throw new MWException( __METHOD__ . ": invalid changeType $t." );
 525+ }
 526+ }
 527+
 528+ function setChangeObject($o) {
 529+ # we assume $o to be a Thread.
 530+ if($o === null) {
 531+ $this->changeObject = null;
 532+ } else {
 533+ $this->changeObject = $o->id();
 534+ }
 535+ }
 536+
 537+ function changeUser() {
 538+ if( $this->changeUser == 0 ) {
 539+ return User::newFromName($this->changeUserText, false);
 540+ } else {
 541+ return User::newFromId($this->changeUser);
 542+ }
 543+ }
 544+
 545+ function changeComment() {
 546+ return $this->changeComment;
 547+ }
 548+
 549+ function redirectThread() {
 550+ $rev = Revision::newFromId($this->root()->getLatest());
 551+ $rtitle = Title::newFromRedirect($rev->getRawText());
 552+ if( !$rtitle ) return null;
 553+ $rthread = Threads::withRoot(new Article($rtitle));
 554+ return $rthread;
 555+ }
 556+
 557+ // Called from hook in Title::isProtected.
 558+ static function getRestrictionsForTitle($title, $action, &$result) {
 559+ $thread = Threads::withRoot(new Post($title));
 560+ if ($thread)
 561+ return $thread->getRestrictions($action, $result);
 562+ else
 563+ return true; // not a thread; do normal protection check.
 564+ }
 565+
 566+ // This only makes sense when called from the hook, because it uses the hook's
 567+ // default behavior to check whether this thread itself is protected, so you'll
 568+ // get false negatives if you use it from some other context.
 569+ function getRestrictions($action, &$result) {
 570+ if( $this->hasSuperthread() ) {
 571+ $parent_restrictions = $this->superthread()->root()->getTitle()->getRestrictions($action);
 572+ } else {
 573+ $parent_restrictions = $this->article()->getTitle()->getTalkPage()->getRestrictions($action);
 574+ }
 575+
 576+ // TODO this may not be the same as asking "are the parent restrictions more restrictive than
 577+ // our own restrictions?", which is what we really want.
 578+ if( count($parent_restrictions) == 0 ) {
 579+ return true; // go to normal protection check.
 580+ } else {
 581+ $result = $parent_restrictions;
 582+ return false;
 583+ }
 584+
 585+ }
 586+}
Index: trunk/extensions/LiquidThreads/classes/LqtHistoricalThread.php
@@ -0,0 +1,66 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class HistoricalThread extends Thread {
 7+ function __construct($t) {
 8+ /* SCHEMA changes must be reflected here. */
 9+ $this->rootId = $t->rootId;
 10+ $this->rootRevision = $t->rootRevision;
 11+ $this->articleId = $t->articleId;
 12+ $this->summaryId = $t->summaryId;
 13+ $this->articleNamespace = $t->articleNamespace;
 14+ $this->articleTitle = $t->articleTitle;
 15+ $this->modified = $t->modified;
 16+ $this->created = $t->created;
 17+ $this->ancestorId = $t->ancestorId;
 18+ $this->parentId = $t->parentId;
 19+ $this->id = $t->id;
 20+ $this->revisionNumber = $t->revisionNumber;
 21+ $this->changeType = $t->changeType;
 22+ $this->changeObject = $t->changeObject;
 23+ $this->changeComment = $t->changeComment;
 24+ $this->changeUser = $t->changeUser;
 25+ $this->changeUserText = $t->changeUserText;
 26+ $this->editedness = $t->editedness;
 27+
 28+ $this->replies = array();
 29+ foreach ($t->replies as $r) {
 30+ $this->replies[] = new HistoricalThread($r);
 31+ }
 32+ }
 33+ static function textRepresentation($t) {
 34+ $ht = new HistoricalThread($t);
 35+ return serialize($ht);
 36+ }
 37+ static function fromTextRepresentation($r) {
 38+ return unserialize($r);
 39+ }
 40+ static function create( $t, $change_type, $change_object ) {
 41+ $tmt = $t->topmostThread();
 42+ $contents = HistoricalThread::textRepresentation($tmt);
 43+ $dbr =& wfGetDB( DB_MASTER );
 44+ $res = $dbr->insert( 'historical_thread', array(
 45+ 'hthread_id'=>$tmt->id(),
 46+ 'hthread_revision'=>$tmt->revisionNumber(),
 47+ 'hthread_contents'=>$contents,
 48+ 'hthread_change_type'=>$tmt->changeType(),
 49+ 'hthread_change_object'=>$tmt->changeObject() ? $tmt->changeObject()->id() : null),
 50+ __METHOD__ );
 51+ }
 52+ static function withIdAtRevision( $id, $rev ) {
 53+ $dbr =& wfGetDB( DB_SLAVE );
 54+ $line = $dbr->selectRow(
 55+ 'historical_thread',
 56+ 'hthread_contents',
 57+ array('hthread_id' => $id, 'hthread_revision' => $rev),
 58+ __METHOD__);
 59+ if ( $line )
 60+ return HistoricalThread::fromTextRepresentation($line->hthread_contents);
 61+ else
 62+ return null;
 63+ }
 64+ function isHistorical() {
 65+ return true;
 66+ }
 67+}
Index: trunk/extensions/LiquidThreads/classes/LqtThreads.php
@@ -0,0 +1,274 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class Threads {
 7+
 8+ const TYPE_NORMAL = 0;
 9+ const TYPE_MOVED = 1;
 10+ const TYPE_DELETED = 2;
 11+ static $VALID_TYPES = array(self::TYPE_NORMAL, self::TYPE_MOVED, self::TYPE_DELETED);
 12+
 13+ const CHANGE_NEW_THREAD = 0;
 14+ const CHANGE_REPLY_CREATED = 1;
 15+ const CHANGE_EDITED_ROOT = 2;
 16+ const CHANGE_EDITED_SUMMARY = 3;
 17+ const CHANGE_DELETED = 4;
 18+ const CHANGE_UNDELETED = 5;
 19+ const CHANGE_MOVED_TALKPAGE = 6;
 20+ static $VALID_CHANGE_TYPES = array(self::CHANGE_EDITED_SUMMARY, self::CHANGE_EDITED_ROOT,
 21+ self::CHANGE_REPLY_CREATED, self::CHANGE_NEW_THREAD, self::CHANGE_DELETED, self::CHANGE_UNDELETED,
 22+ self::CHANGE_MOVED_TALKPAGE);
 23+
 24+ // Possible values of Thread->editedness.
 25+ const EDITED_NEVER = 0;
 26+ const EDITED_HAS_REPLY = 1;
 27+ const EDITED_BY_AUTHOR = 2;
 28+ const EDITED_BY_OTHERS = 3;
 29+
 30+ static $cache_by_root = array();
 31+ static $cache_by_id = array();
 32+
 33+ static function newThread( $root, $article, $superthread = null, $type = self::TYPE_NORMAL ) {
 34+ // SCHEMA changes must be reflected here.
 35+ // TODO: It's dumb that the commitRevision code isn't used here.
 36+
 37+ $dbr =& wfGetDB( DB_MASTER );
 38+
 39+ if ( !in_array($type, self::$VALID_TYPES) ) {
 40+ throw new MWException(__METHOD__ . ": invalid type $type.");
 41+ }
 42+
 43+ if ($superthread) {
 44+ $change_type = self::CHANGE_REPLY_CREATED;
 45+ } else {
 46+ $change_type = self::CHANGE_NEW_THREAD;
 47+ }
 48+
 49+ global $wgUser; // TODO global.
 50+
 51+ $timestamp = wfTimestampNow();
 52+
 53+ $res = $dbr->insert('thread',
 54+ array('thread_root' => $root->getID(),
 55+ 'thread_parent' => $superthread ? $superthread->id() : null,
 56+ 'thread_article_namespace' => $article->getTitle()->getNamespace(),
 57+ 'thread_article_title' => $article->getTitle()->getDBkey(),
 58+ 'thread_modified' => $timestamp,
 59+ 'thread_created' => $timestamp,
 60+ 'thread_change_type' => $change_type,
 61+ 'thread_change_comment' => "", // TODO
 62+ 'thread_change_user' => $wgUser->getID(),
 63+ 'thread_change_user_text' => $wgUser->getName(),
 64+ 'thread_type' => $type,
 65+ 'thread_editedness' => self::EDITED_NEVER),
 66+ __METHOD__);
 67+
 68+ $newid = $dbr->insertId();
 69+
 70+ if( $superthread ) {
 71+ $ancestor = $superthread->ancestorId();
 72+ $change_object_clause = 'thread_change_object = ' . $newid;
 73+ } else {
 74+ $ancestor = $newid;
 75+ $change_object_clause = 'thread_change_object = null';
 76+ }
 77+ $res = $dbr->update( 'thread',
 78+ /* SET */ array( 'thread_ancestor' => $ancestor,
 79+ $change_object_clause ),
 80+ /* WHERE */ array( 'thread_id' => $newid, ),
 81+ __METHOD__);
 82+
 83+ // TODO we could avoid a query here.
 84+ $newthread = Threads::withId($newid);
 85+ if($superthread) {
 86+ $superthread->addReply( $newthread );
 87+ }
 88+
 89+ self::createTalkpageIfNeeded($article);
 90+
 91+ NewMessages::writeMessageStateForUpdatedThread($newthread);
 92+
 93+ return $newthread;
 94+ }
 95+
 96+ /**
 97+ * Create the talkpage if it doesn't exist so that links to it
 98+ * will show up blue instead of red. For use upon new thread creation.
 99+ */
 100+ protected static function createTalkpageIfNeeded($subjectPage) {
 101+ $talkpage_t = $subjectPage->getTitle()->getTalkpage();
 102+ $talkpage = new Article($talkpage_t);
 103+ if( ! $talkpage->exists() ) {
 104+ try {
 105+ wfLoadExtensionMessages( 'LiquidThreads' );
 106+ $talkpage->doEdit( "", wfMsg('lqt_talkpage_autocreate_summary'), EDIT_NEW | EDIT_SUPPRESS_RC );
 107+
 108+ } catch( DBQueryError $e ) {
 109+ // The article already existed by now. No need to do anything.
 110+ wfDebug(__METHOD__ . ": Article already existed by the time we tried to create it.");
 111+ }
 112+ }
 113+ }
 114+
 115+ static function where( $where, $options = array(), $extra_tables = array(), $joins = "" ) {
 116+ global $wgDBprefix;
 117+ $dbr = wfGetDB( DB_SLAVE );
 118+ if ( is_array($where) ) $where = $dbr->makeList( $where, LIST_AND );
 119+ if ( is_array($options) ) $options = implode(',', $options);
 120+
 121+ if( is_array($extra_tables) && count($extra_tables) != 0 ) {
 122+ if(!empty($wgDBprefix)) {
 123+ foreach($extra_tables as $tablekey=>$extra_table)
 124+ $extra_tables[$tablekey]=$wgDBprefix.$extra_table;
 125+ }
 126+ $tables = implode(',', $extra_tables) . ', ';
 127+ } else if ( is_string( $extra_tables ) ) {
 128+ $tables = $extra_tables . ', ';
 129+ } else {
 130+ $tables = "";
 131+ }
 132+
 133+
 134+ $selection_sql = <<< SQL
 135+ SELECT DISTINCT thread.* FROM ($tables {$wgDBprefix}thread thread)
 136+ $joins
 137+ WHERE $where
 138+ $options
 139+SQL;
 140+ $selection_res = $dbr->query($selection_sql);
 141+
 142+ $ancestor_conds = array();
 143+ $selection_conds = array();
 144+ while( $line = $dbr->fetchObject($selection_res) ) {
 145+ $ancestor_conds[] = $line->thread_ancestor;
 146+ $selection_conds[] = $line->thread_id;
 147+ }
 148+ if( count($selection_conds) == 0 ) {
 149+ // No threads were found, so we can skip the second query.
 150+ return array();
 151+ } // List comprehensions, how I miss thee.
 152+ $ancestor_clause = join(', ', $ancestor_conds);
 153+ $selection_clause = join(', ', $selection_conds);
 154+
 155+ $children_sql = <<< SQL
 156+ SELECT DISTINCT thread.*, page.*,
 157+ thread.thread_id IN($selection_clause) as selected
 158+ FROM ({$wgDBprefix}thread thread, {$wgDBprefix}page page)
 159+ WHERE thread.thread_ancestor IN($ancestor_clause)
 160+ AND page.page_id = thread.thread_root
 161+ $options
 162+SQL;
 163+ $res = $dbr->query($children_sql);
 164+
 165+ $threads = array();
 166+ $top_level_threads = array();
 167+ $thread_children = array();
 168+
 169+ while ( $line = $dbr->fetchObject($res) ) {
 170+ $new_thread = new Thread($line, null);
 171+ $threads[] = $new_thread;
 172+ if( $line->selected )
 173+ // thread is one of those that was directly queried for.
 174+ $top_level_threads[] = $new_thread;
 175+ if( $line->thread_parent !== null ) {
 176+ if( !array_key_exists( $line->thread_parent, $thread_children ) )
 177+ $thread_children[$line->thread_parent] = array();
 178+ // Can have duplicate if thread is both top_level and child of another top_level thread.
 179+ if( !self::arrayContainsThreadWithId($thread_children[$line->thread_parent], $new_thread->id()) )
 180+ $thread_children[$line->thread_parent][] = $new_thread;
 181+ }
 182+ }
 183+
 184+ foreach( $threads as $thread ) {
 185+ if( array_key_exists( $thread->id(), $thread_children ) ) {
 186+ $thread->initWithReplies( $thread_children[$thread->id()] );
 187+ } else {
 188+ $thread->initWithReplies( array() );
 189+ }
 190+
 191+ self::$cache_by_root[$thread->root()->getID()] = $thread;
 192+ self::$cache_by_id[$thread->id()] = $thread;
 193+ }
 194+
 195+ return $top_level_threads;
 196+ }
 197+
 198+ private static function databaseError( $msg ) {
 199+ // TODO tie into MW's error reporting facilities.
 200+ echo("Corrupt liquidthreads database: $msg");
 201+ die();
 202+ }
 203+
 204+ private static function assertSingularity( $threads, $attribute, $value ) {
 205+ if( count($threads) == 0 ) { return null; }
 206+ if( count($threads) == 1 ) { return $threads[0]; }
 207+ if( count($threads) > 1 ) {
 208+ Threads::databaseError("More than one thread with $attribute = $value.");
 209+ return null;
 210+ }
 211+ }
 212+
 213+ private static function arrayContainsThreadWithId( $a, $id ) {
 214+ // There's gotta be a nice way to express this in PHP. Anyone?
 215+ foreach($a as $t)
 216+ if($t->id() == $id)
 217+ return true;
 218+ return false;
 219+ }
 220+
 221+ static function withRoot( $post ) {
 222+ if( $post->getTitle()->getNamespace() != NS_LQT_THREAD ) {
 223+ // No articles outside the thread namespace have threads associated with them;
 224+ // avoiding the query saves time during the TitleGetRestrictions hook.
 225+ return null;
 226+ }
 227+ if( array_key_exists( $post->getID(), self::$cache_by_root ) ) {
 228+ return self::$cache_by_root[$post->getID()];
 229+ }
 230+ $ts = Threads::where( array('thread.thread_root' => $post->getID()) );
 231+ return self::assertSingularity($ts, 'thread_root', $post->getID());
 232+ }
 233+
 234+ static function withId( $id ) {
 235+ if( array_key_exists( $id, self::$cache_by_id ) ) {
 236+ return self::$cache_by_id[$id];
 237+ }
 238+ $ts = Threads::where( array('thread.thread_id' => $id ) );
 239+ return self::assertSingularity($ts, 'thread_id', $id);
 240+ }
 241+
 242+ static function withSummary( $article ) {
 243+ $ts = Threads::where( array('thread.thread_summary_page' => $article->getId()));
 244+ return self::assertSingularity($ts, 'thread_summary_page', $article->getId());
 245+ }
 246+
 247+ /**
 248+ * Horrible, horrible!
 249+ * List of months in which there are >0 threads, suitable for threadsOfArticleInMonth. */
 250+ static function monthsWhereArticleHasThreads( $article ) {
 251+ $threads = Threads::where( Threads::articleClause($article) );
 252+ $months = array();
 253+ foreach( $threads as $t ) {
 254+ $m = substr( $t->modified(), 0, 6 );
 255+ if ( !array_key_exists( $m, $months ) ) {
 256+ if (!in_array( $m, $months )) $months[] = $m;
 257+ }
 258+ }
 259+ return $months;
 260+ }
 261+
 262+ static function articleClause($article) {
 263+ $dbr = wfGetDB(DB_SLAVE);
 264+ $q_article= $dbr->addQuotes($article->getTitle()->getDBkey());
 265+ return <<<SQL
 266+(thread.thread_article_title = $q_article
 267+ AND thread.thread_article_namespace = {$article->getTitle()->getNamespace()})
 268+SQL;
 269+ }
 270+
 271+ static function topLevelClause() {
 272+ return 'thread.thread_parent is null';
 273+ }
 274+
 275+}
Index: trunk/extensions/LiquidThreads/pages/NewUserMessagesView.php
@@ -0,0 +1,167 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class NewUserMessagesView extends LqtView {
 7+
 8+ protected $threads;
 9+ protected $tops;
 10+ protected $targets;
 11+
 12+ protected function htmlForReadButton($label, $title, $class, $ids) {
 13+ $ids_s = implode(',', $ids);
 14+ return <<<HTML
 15+ <form method="POST" class="{$class}">
 16+ <input type="hidden" name="lqt_method" value="mark_as_read" />
 17+ <input type="hidden" name="lqt_operand" value="{$ids_s}" />
 18+ <input type="submit" value="{$label}" name="lqt_read_button" title="{$title}" />
 19+ </form>
 20+HTML;
 21+ }
 22+
 23+ function showReadAllButton($threads) {
 24+ wfLoadExtensionMessages( 'LiquidThreads' );
 25+ $ids = array_map(create_function('$t', 'return $t->id();'), $threads);
 26+ $this->output->addHTML(
 27+ $this->htmlForReadButton(
 28+ wfMsg('lqt-read-all'),
 29+ wfMsg('lqt-read-all-tooltip'),
 30+ "lqt_newmessages_read_all_button",
 31+ $ids )
 32+ );
 33+ }
 34+
 35+ function preShowThread($t) {
 36+ wfLoadExtensionMessages( 'LiquidThreads' );
 37+ // $t_ids = implode(',', array_map(create_function('$t', 'return $t->id();'), $this->targets[$t->id()]));
 38+ $read_button = $this->htmlForReadButton(
 39+ wfMsg('lqt-read-message'),
 40+ wfMsg('lqt-read-message-tooltip'),
 41+ 'lqt_newmessages_read_button',
 42+ $this->targets[$t->id()]);
 43+ $this->output->addHTML(<<<HTML
 44+<table ><tr>
 45+<td style="padding-right: 1em; vertical-align: top; padding-top: 1em;" >
 46+$read_button
 47+</td>
 48+<td>
 49+HTML
 50+ );
 51+ }
 52+
 53+ function postShowThread($t) {
 54+ $this->output->addHTML(<<<HTML
 55+</td>
 56+</tr></table>
 57+HTML
 58+ );
 59+ }
 60+
 61+ function showUndo($ids) {
 62+ wfLoadExtensionMessages( 'LiquidThreads' );
 63+ if( count($ids) == 1 ) {
 64+ $t = Threads::withId($ids[0]);
 65+ if( !$t )
 66+ return; // empty or just bogus operand.
 67+ $msg = wfMsg( 'lqt-marked-read',$t->subject() );
 68+ } else {
 69+ $count = count($ids);
 70+ $msg = wfMsg( 'lqt-count-marked-read',$count );
 71+ }
 72+ $operand = implode(',', $ids);
 73+ $lqt_email_undo = wfMsg ( 'lqt-email-undo' );
 74+ $lqt_info_undo = wfMsg ( 'lqt-email-info-undo' );
 75+ $this->output->addHTML(<<<HTML
 76+<form method="POST" class="lqt_undo_mark_as_read">
 77+$msg
 78+<input type="hidden" name="lqt_method" value="mark_as_unread" />
 79+<input type="hidden" name="lqt_operand" value="{$operand}" />
 80+<input type="submit" value="{$lqt_email_undo}" name="lqt_read_button" title="{$lqt_info_undo}" />
 81+</form>
 82+HTML
 83+ );
 84+ }
 85+
 86+ function postDivClass($thread) {
 87+ $topid = $thread->topmostThread()->id();
 88+ if( in_array($thread->id(), $this->targets[$topid]) )
 89+ return 'lqt_post_new_message';
 90+ else
 91+ return 'lqt_post';
 92+ }
 93+
 94+ function showOnce() {
 95+ self::addJSandCSS();
 96+
 97+ if( $this->request->wasPosted() ) {
 98+ // If they just viewed this page, maybe they still want that notice.
 99+ // But if they took the time to dismiss even one message, they
 100+ // probably don't anymore.
 101+ $this->user->setNewtalk(false);
 102+ }
 103+
 104+ if( $this->request->wasPosted() && $this->methodApplies('mark_as_unread') ) {
 105+ $ids = explode(',', $this->request->getVal('lqt_operand', ''));
 106+ if( $ids !== false ) {
 107+ foreach($ids as $id) {
 108+ $tmp_thread = Threads::withId($id); if($tmp_thread)
 109+ NewMessages::markThreadAsReadByUser($tmp_thread, $this->user);
 110+ }
 111+ $this->output->redirect( $this->title->getFullURL() );
 112+ }
 113+ }
 114+
 115+ else if( $this->request->wasPosted() && $this->methodApplies('mark_as_read') ) {
 116+ $ids = explode(',', $this->request->getVal('lqt_operand'));
 117+ if( $ids !== false ) {
 118+ foreach($ids as $id) {
 119+ $tmp_thread = Threads::withId($id); if($tmp_thread)
 120+ NewMessages::markThreadAsReadByUser($tmp_thread, $this->user);
 121+ }
 122+ $query = 'lqt_method=undo_mark_as_read&lqt_operand=' . implode(',', $ids);
 123+ $this->output->redirect( $this->title->getFullURL($query) );
 124+ }
 125+ }
 126+
 127+ else if( $this->methodApplies('undo_mark_as_read') ) {
 128+ $ids = explode(',', $this->request->getVal('lqt_operand', ''));
 129+ $this->showUndo($ids);
 130+ }
 131+ }
 132+
 133+ function show() {
 134+ if ( ! is_array( $this->threads ) ) {
 135+ throw new MWException('You must use NewUserMessagesView::setThreads() before calling NewUserMessagesView::show().');
 136+ }
 137+
 138+ // Do everything by id, because we can't depend on reference identity; a simple Thread::withId
 139+ // can change the cached value and screw up your references.
 140+
 141+ $this->targets = array();
 142+ $this->tops = array();
 143+ foreach( $this->threads as $t ) {
 144+ $top = $t->topmostThread();
 145+ if( !in_array($top->id(), $this->tops) )
 146+ $this->tops[] = $top->id();
 147+ if( !array_key_exists($top->id(), $this->targets) )
 148+ $this->targets[$top->id()] = array();
 149+ $this->targets[$top->id()][] = $t->id();
 150+ }
 151+
 152+ foreach($this->tops as $t_id) {
 153+ $t = Threads::withId($t_id);
 154+ // It turns out that with lqtviews composed of threads from various talkpages,
 155+ // each thread is going to have a different article... this is pretty ugly.
 156+ $this->article = $t->article();
 157+
 158+ $this->preShowThread($t);
 159+ $this->showThread($t);
 160+ $this->postShowThread($t);
 161+ }
 162+ return false;
 163+ }
 164+
 165+ function setThreads( $threads ) {
 166+ $this->threads = $threads;
 167+ }
 168+}
Index: trunk/extensions/LiquidThreads/pages/SpecialDeleteThread.php
@@ -0,0 +1,133 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class SpecialDeleteThread extends UnlistedSpecialPage {
 7+ private $user, $output, $request, $title, $thread;
 8+
 9+ function __construct() {
 10+ parent::__construct( 'Deletethread' );
 11+ $this->includable( false );
 12+ }
 13+
 14+ /**
 15+ * @see SpecialPage::getDescription
 16+ */
 17+ function getDescription() {
 18+ wfLoadExtensionMessages( 'LiquidThreads' );
 19+ return wfMsg( 'lqt_deletethread' );
 20+ }
 21+
 22+ function handleGet() {
 23+ if( !$this->checkUserRights() ) return;
 24+ wfLoadExtensionMessages( 'LiquidThreads' );
 25+
 26+ $form_action = $this->title->getLocalURL() . '/' . $this->thread->title()->getPrefixedURL();
 27+ $thread_name = $this->thread->title()->getPrefixedText();
 28+ $article_name = $this->thread->article()->getTitle()->getTalkPage()->getPrefixedText();
 29+
 30+ $deleting = $this->thread->type() != Threads::TYPE_DELETED;
 31+
 32+ $operation_message = $deleting ?
 33+ wfMsg('lqt_delete_deleting', "<b>$thread_name</b>", '<b>'.wfMsg('lqt_delete_deleting_allreplies').'</b>')
 34+ // "Deleting <b>$thread_name</b> and <b>all replies</b> to it."
 35+ : wfMsg('lqt_delete_undeleting', "<b>$thread_name</b>");
 36+ $button_label = $deleting ?
 37+ wfMsg('lqt_delete_deletethread')
 38+ : wfMsg('lqt_delete_undeletethread');
 39+ $part_of = wfMsg('lqt_delete_partof', '<b>'.$article_name.'</b>');
 40+ $reason = wfMsg('movereason'); // XXX arguably wrong to use movereason.
 41+
 42+ $this->output->addHTML(<<<HTML
 43+<p>$operation_message
 44+$part_of</p>
 45+<form id="lqt_delete_thread_form" action="{$form_action}" method="POST">
 46+<table>
 47+<tr>
 48+<td><label for="lqt_delete_thread_reason">$reason</label></td>
 49+<td><input id="lqt_delete_thread_reason" name="lqt_delete_thread_reason" tabindex="200" size="40" /></td>
 50+</tr><tr>
 51+<td>&nbsp;</td>
 52+<td><input type="submit" value="$button_label" style="float:right;" tabindex="300" /></td>
 53+</tr>
 54+</table>
 55+</form>
 56+HTML
 57+ );
 58+
 59+ }
 60+
 61+ function checkUserRights() {
 62+ if( in_array('delete', $this->user->getRights()) ) {
 63+ return true;
 64+ } else {
 65+ wfLoadExtensionMessages( 'LiquidThreads' );
 66+ $this->output->addHTML(wfMsg('lqt_delete_unallowed'));
 67+ return false;
 68+ }
 69+ }
 70+
 71+ function redisplayForm($problem_fields, $message) {
 72+ $this->output->addHTML($message);
 73+ $this->handleGet();
 74+ }
 75+
 76+ function handlePost() {
 77+ // in theory the model should check rights...
 78+ if( !$this->checkUserRights() ) return;
 79+ wfLoadExtensionMessages( 'LiquidThreads' );
 80+
 81+ $reason = $this->request->getVal('lqt_delete_thread_reason', wfMsg('lqt_noreason'));
 82+
 83+ // TODO: in theory, two fast-acting sysops could undo each others' work.
 84+ $is_deleted_already = $this->thread->type() == Threads::TYPE_DELETED;
 85+ if ( $is_deleted_already ) {
 86+ $this->thread->undelete($reason);
 87+ } else {
 88+ $this->thread->delete($reason);
 89+ }
 90+ $this->showSuccessMessage( $is_deleted_already );
 91+ }
 92+
 93+ function showSuccessMessage( $is_deleted_already ) {
 94+ wfLoadExtensionMessages( 'LiquidThreads' );
 95+ // TODO talkpageUrl should accept threads, and look up their talk pages.
 96+ $talkpage_url = LqtView::talkpageUrl($this->thread->article()->getTitle()->getTalkpage());
 97+ $message = $is_deleted_already ? wfMsg('lqt_delete_undeleted') : wfMsg('lqt_delete_deleted');
 98+ $message .= ' ';
 99+ $message .= wfMsg('lqt_delete_return', '<a href="'.$talkpage_url.'">'.wfMsg('lqt_delete_return_link').'</a>');
 100+ $this->output->addHTML($message);
 101+ }
 102+
 103+ function execute( $par ) {
 104+ global $wgOut, $wgRequest, $wgTitle, $wgUser;
 105+ $this->user = $wgUser;
 106+ $this->output = $wgOut;
 107+ $this->request = $wgRequest;
 108+ $this->title = $wgTitle;
 109+
 110+ $this->setHeaders();
 111+
 112+ if( $par === null || $par === "") {
 113+ wfLoadExtensionMessages( 'LiquidThreads' );
 114+ $this->output->addHTML(wfMsg('lqt_threadrequired'));
 115+ return;
 116+ }
 117+ // TODO should implement Threads::withTitle(...).
 118+ $thread = Threads::withRoot( new Article(Title::newFromURL($par)) );
 119+ if (!$thread) {
 120+ wfLoadExtensionMessages( 'LiquidThreads' );
 121+ $this->output->addHTML(wfMsg('lqt_nosuchthread'));
 122+ return;
 123+ }
 124+
 125+ $this->thread = $thread;
 126+
 127+ if ( $this->request->wasPosted() ) {
 128+ $this->handlePost();
 129+ } else {
 130+ $this->handleGet();
 131+ }
 132+
 133+ }
 134+}
Index: trunk/extensions/LiquidThreads/pages/ThreadDiffView.php
@@ -0,0 +1,22 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class ThreadDiffView {
 7+ function customizeTabs( $skintemplate, $content_actions ) {
 8+ unset($content_actions['edit']);
 9+ unset($content_actions['viewsource']);
 10+ unset($content_actions['talk']);
 11+
 12+ $content_actions['talk']['class'] = false;
 13+ $content_actions['history']['class'] = 'selected';
 14+
 15+ return true;
 16+ }
 17+
 18+ function show() {
 19+ global $wgHooks;
 20+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 21+ return true;
 22+ }
 23+}
Index: trunk/extensions/LiquidThreads/pages/TalkpageHeaderView.php
@@ -0,0 +1,40 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class TalkpageHeaderView extends LqtView {
 7+ function customizeTabs( $skintemplate, $content_actions ) {
 8+ unset($content_actions['edit']);
 9+ unset($content_actions['addsection']);
 10+ unset($content_actions['history']);
 11+ unset($content_actions['watch']);
 12+ unset($content_actions['move']);
 13+
 14+ $content_actions['talk']['class'] = false;
 15+ $content_actions['header'] = array( 'class'=>'selected',
 16+ 'text'=>'header',
 17+ 'href'=>'');
 18+
 19+ return true;
 20+ }
 21+
 22+ function show() {
 23+ global $wgHooks, $wgOut, $wgTitle, $wgRequest;
 24+ // Why is a hook added here?
 25+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 26+
 27+ if( $wgRequest->getVal('action') === 'edit' ) {
 28+ wfLoadExtensionMessages( 'LiquidThreads' );
 29+ $warn_bold = '<strong>' . wfMsg('lqt_header_warning_bold') . '</strong>';
 30+ $warn_link = '<a href="'.$this->talkpageUrl($wgTitle, 'talkpage_new_thread').'">'.
 31+ wfMsg('lqt_header_warning_new_discussion').'</a>';
 32+ $wgOut->addHTML('<p class="lqt_header_warning">' .
 33+ wfMsg('lqt_header_warning_before_big', $warn_bold, $warn_link) .
 34+ '<big>' . wfMsg('lqt_header_warning_big', $warn_bold, $warn_link) . '</big>' .
 35+ wfMsg('lqt_header_warning_after_big', $warn_bold, $warn_link) .
 36+ '</p>');
 37+ }
 38+
 39+ return true;
 40+ }
 41+}
Index: trunk/extensions/LiquidThreads/pages/IndividualThreadHistoryView.php
@@ -0,0 +1,47 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class IndividualThreadHistoryView extends ThreadPermalinkView {
 7+ protected $oldid;
 8+
 9+ function customizeTabs( $skintemplate, $content_actions ) {
 10+ $content_actions['history']['class'] = 'selected';
 11+ parent::customizeTabs($skintemplate, $content_actions);
 12+ return true;
 13+ }
 14+
 15+ /* This customizes the subtitle of a history *listing* from the hook,
 16+ and of an old revision from getSubtitle() below. */
 17+ function customizeSubtitle() {
 18+ wfLoadExtensionMessages( 'LiquidThreads' );
 19+ $msg = wfMsg('lqt_hist_view_whole_thread');
 20+ $threadhist = "<a href=\"{$this->permalinkUrl($this->thread->topmostThread(), 'thread_history')}\">$msg</a>";
 21+ $this->output->setSubtitle( parent::getSubtitle() . '<br />' . $this->output->getSubtitle() . "<br />$threadhist" );
 22+ return true;
 23+ }
 24+
 25+ /* */
 26+ function getSubtitle() {
 27+ $this->article->setOldSubtitle($this->oldid);
 28+ $this->customizeSubtitle();
 29+ return $this->output->getSubtitle();
 30+ }
 31+
 32+ function show() {
 33+ global $wgHooks;
 34+ /*
 35+ $this->oldid = $this->request->getVal('oldid', null);
 36+ if( $this->oldid !== null ) {
 37+
 38+ parent::show();
 39+ return false;
 40+ }
 41+ */
 42+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 43+
 44+ $wgHooks['PageHistoryBeforeList'][] = array($this, 'customizeSubtitle');
 45+
 46+ return true;
 47+ }
 48+}
Index: trunk/extensions/LiquidThreads/pages/ThreadProtectionFormView.php
@@ -0,0 +1,26 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class ThreadProtectionFormView {
 7+ function customizeTabs( $skintemplate, $content_actions ) {
 8+ unset($content_actions['edit']);
 9+ unset($content_actions['addsection']);
 10+ unset($content_actions['viewsource']);
 11+ unset($content_actions['talk']);
 12+
 13+ $content_actions['talk']['class'] = false;
 14+ if ( array_key_exists('protect', $content_actions) )
 15+ $content_actions['protect']['class'] = 'selected';
 16+ else if ( array_key_exists('unprotect', $content_actions) )
 17+ $content_actions['unprotect']['class'] = 'selected';
 18+
 19+ return true;
 20+ }
 21+
 22+ function show() {
 23+ global $wgHooks;
 24+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 25+ return true;
 26+ }
 27+}
Index: trunk/extensions/LiquidThreads/pages/ThreadHistoryListingView.php
@@ -0,0 +1,107 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class ThreadHistoryListingView extends ThreadPermalinkView {
 7+
 8+ private function rowForThread($t) {
 9+ global $wgLang, $wgOut; // TODO global.
 10+ wfLoadExtensionMessages( 'LiquidThreads' );
 11+ /* TODO: best not to refer to LqtView class directly. */
 12+ /* We don't use oldid because that has side-effects. */
 13+ $result = array();
 14+ $change_names = array( Threads::CHANGE_EDITED_ROOT => wfMsg('lqt_hist_comment_edited'),
 15+ Threads::CHANGE_EDITED_SUMMARY => wfMsg('lqt_hist_summary_changed'),
 16+ Threads::CHANGE_REPLY_CREATED => wfMsg('lqt_hist_reply_created'),
 17+ Threads::CHANGE_NEW_THREAD => wfMsg('lqt_hist_thread_created'),
 18+ Threads::CHANGE_DELETED => wfMsg('lqt_hist_deleted'),
 19+ Threads::CHANGE_UNDELETED => wfMsg('lqt_hist_undeleted'),
 20+ Threads::CHANGE_MOVED_TALKPAGE => wfMsg('lqt_hist_moved_talkpage'));
 21+ $change_label = array_key_exists($t->changeType(), $change_names) ? $change_names[$t->changeType()] : "";
 22+
 23+ $url = LqtView::permalinkUrlWithQuery( $this->thread, 'lqt_oldid=' . $t->revisionNumber() );
 24+
 25+ $user_id = $t->changeUser()->getID(); # ever heard of a User object?
 26+ $user_text = $t->changeUser()->getName();
 27+ $sig = $this->user->getSkin()->userLink( $user_id, $user_text ) .
 28+ $this->user->getSkin()->userToolLinks( $user_id, $user_text );
 29+
 30+ $change_comment=$t->changeComment();
 31+ if(!empty($change_comment))
 32+ $change_comment="<em>($change_comment)</em>";
 33+
 34+ $result[] = "<tr>";
 35+ $result[] = "<td><a href=\"$url\">" . $wgLang->timeanddate($t->modified()) . "</a></td>";
 36+ $result[] = "<td>" . $sig . "</td>";
 37+ $result[] = "<td>$change_label</td>";
 38+ $result[] = "<td>$change_comment</td>";
 39+ $result[] = "</tr>";
 40+ return implode('', $result);
 41+ }
 42+
 43+ function showHistoryListing($t) {
 44+ wfLoadExtensionMessages( 'LiquidThreads' );
 45+ $revisions = new ThreadHistoryIterator($t, $this->perPage, $this->perPage * ($this->page - 1));
 46+
 47+ $this->output->addHTML('<table>');
 48+ foreach($revisions as $ht) {
 49+ $this->output->addHTML($this->rowForThread($ht));
 50+ }
 51+ $this->output->addHTML('</table>');
 52+
 53+ if ( count($revisions) == 0 && $this->page == 1 ) {
 54+ $this->output->addHTML('<p>'.wfMsg('lqt_hist_no_revisions_error'));
 55+ }
 56+ else if ( count($revisions) == 0 ) {
 57+ // we could redirect to the previous page... yow.
 58+ $this->output->addHTML('<p>'.wfMsg('lqt_hist_past_last_page_error'));
 59+ }
 60+
 61+ if( $this->page > 1 ) {
 62+ $this->output->addHTML( '<a class="lqt_newer_older" href="' . $this->queryReplace(array('lqt_hist_page'=>$this->page - 1)) .'">'.wfMsg('lqt_newer').'</a>' );
 63+ } else {
 64+ $this->output->addHTML( '<span class="lqt_newer_older_disabled" title="'.wfMsg('lqt_hist_tooltip_newer_disabled').'">'.wfMsg('lqt_newer').'</span>' );
 65+ }
 66+
 67+ $is_last_page = false;
 68+ foreach($revisions as $r)
 69+ if( $r->changeType() == Threads::CHANGE_NEW_THREAD )
 70+ $is_last_page = true;
 71+ if( $is_last_page ) {
 72+ $this->output->addHTML( '<span class="lqt_newer_older_disabled" title="'.wfMsg('lqt_hist_tooltip_older_disabled').'">'.wfMsg('lqt_older').'</span>' );
 73+ } else {
 74+ $this->output->addHTML( '<a class="lqt_newer_older" href="' . $this->queryReplace(array('lqt_hist_page'=>$this->page + 1)) . '">'.wfMsg('lqt_older').'</a>' );
 75+ }
 76+ }
 77+
 78+ function __construct(&$output, &$article, &$title, &$user, &$request) {
 79+ parent::__construct($output, $article, $title, $user, $request);
 80+ $this->loadParametersFromRequest();
 81+ }
 82+
 83+ function loadParametersFromRequest() {
 84+ $this->perPage = $this->request->getInt('lqt_hist_per_page', 10);
 85+ $this->page = $this->request->getInt('lqt_hist_page', 1);
 86+ }
 87+
 88+ function show() {
 89+ global $wgHooks;
 90+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 91+
 92+ if( ! $this->thread ) {
 93+ $this->showMissingThreadPage();
 94+ return false;
 95+ }
 96+ self::addJSandCSS();
 97+ wfLoadExtensionMessages( 'LiquidThreads' );
 98+
 99+ $this->output->setSubtitle($this->getSubtitle() . '<br />' . wfMsg('lqt_hist_listing_subtitle'));
 100+
 101+ $this->showThreadHeading($this->thread);
 102+ $this->showHistoryListing($this->thread);
 103+
 104+ $this->showThread($this->thread);
 105+
 106+ return false;
 107+ }
 108+}
Index: trunk/extensions/LiquidThreads/pages/SpecialMoveThread.php
@@ -0,0 +1,132 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class SpecialMoveThread extends UnlistedSpecialPage {
 7+ private $user, $output, $request, $title, $thread;
 8+
 9+ function __construct() {
 10+ parent::__construct( 'Movethread' );
 11+ $this->includable( false );
 12+ }
 13+
 14+ /**
 15+ * @see SpecialPage::getDescription
 16+ */
 17+ function getDescription() {
 18+ wfLoadExtensionMessages( 'LiquidThreads' );
 19+ return wfMsg( 'lqt_movethread' );
 20+ }
 21+
 22+ function handleGet() {
 23+ wfLoadExtensionMessages( 'LiquidThreads' );
 24+ $form_action = $this->title->getLocalURL() . '/' . $this->thread->title()->getPrefixedURL();
 25+ $thread_name = $this->thread->title()->getPrefixedText();
 26+ $article_name = $this->thread->article()->getTitle()->getTalkPage()->getPrefixedText();
 27+ $edit_url = LqtView::permalinkUrl($this->thread, 'edit', $this->thread);
 28+ $wfMsg = 'wfMsg'; // functions can only be called within string expansion by variable name.
 29+ $this->output->addHTML(<<<HTML
 30+<p>{$wfMsg('lqt_move_movingthread', "<b>$thread_name</b>", "<b>$article_name</b>")}</p>
 31+<p>{$wfMsg('lqt_move_torename', "<a href=\"$edit_url\">{$wfMsg('lqt_move_torename_edit')}</a>")}</p>
 32+<form id="lqt_move_thread_form" action="$form_action" method="POST">
 33+<table>
 34+<tr>
 35+<td><label for="lqt_move_thread_target_title">{$wfMsg('lqt_move_destinationtitle')}</label></td>
 36+<td><input id="lqt_move_thread_target_title" name="lqt_move_thread_target_title" tabindex="100" size="40" /></td>
 37+</tr><tr>
 38+<td><label for="lqt_move_thread_reason">{$wfMsg('movereason')}</label></td>
 39+<td><input id="lqt_move_thread_reason" name="lqt_move_thread_reason" tabindex="200" size="40" /></td>
 40+</tr><tr>
 41+<td>&nbsp;</td>
 42+<td><input type="submit" value="{$wfMsg('lqt_move_move')}" style="float:right;" tabindex="300" /></td>
 43+</tr>
 44+</table>
 45+</form>
 46+HTML
 47+ );
 48+
 49+ }
 50+
 51+ function checkUserRights() {
 52+ if ( !$this->user->isAllowed( 'move' ) ) {
 53+ $this->output->showErrorPage( 'movenologin', 'movenologintext' );
 54+ return false;
 55+ }
 56+ if ( $this->user->isBlocked() ) {
 57+ $this->output->blockedPage();
 58+ return false;
 59+ }
 60+ if ( wfReadOnly() ) {
 61+ $this->output->readOnlyPage();
 62+ return false;
 63+ }
 64+ if ( $this->user->pingLimiter( 'move' ) ) {
 65+ $this->output->rateLimited();
 66+ return false;
 67+ }
 68+ /* Am I forgetting anything? */
 69+ return true;
 70+ }
 71+
 72+ function redisplayForm($problem_fields, $message) {
 73+ $this->output->addHTML($message);
 74+ $this->handleGet();
 75+ }
 76+
 77+ function handlePost() {
 78+ if( !$this->checkUserRights() ) return;
 79+ wfLoadExtensionMessages( 'LiquidThreads' );
 80+
 81+ $tmp = $this->request->getVal('lqt_move_thread_target_title');
 82+ if( $tmp === "" ) {
 83+ $this->redisplayForm(array('lqt_move_thread_target_title'), wfMsg('lqt_move_nodestination'));
 84+ return;
 85+ }
 86+ $newtitle = Title::newFromText( $tmp )->getSubjectPage();
 87+
 88+ $reason = $this->request->getVal('lqt_move_thread_reason', wfMsg('lqt_noreason'));
 89+
 90+ // TODO no status code from this method.
 91+ $this->thread->moveToSubjectPage( $newtitle, $reason, true );
 92+
 93+ $this->showSuccessMessage( $newtitle->getTalkPage() );
 94+ }
 95+
 96+ function showSuccessMessage( $target_title ) {
 97+ wfLoadExtensionMessages( 'LiquidThreads' );
 98+ $this->output->addHTML(wfMsg('lqt_move_success',
 99+ '<a href="'.$target_title->getFullURL().'">'.$target_title->getPrefixedText().'</a>'));
 100+ }
 101+
 102+ function execute( $par ) {
 103+ global $wgOut, $wgRequest, $wgTitle, $wgUser;
 104+ $this->user = $wgUser;
 105+ $this->output = $wgOut;
 106+ $this->request = $wgRequest;
 107+ $this->title = $wgTitle;
 108+
 109+ $this->setHeaders();
 110+
 111+ if( $par === null || $par === "") {
 112+ wfLoadExtensionMessages( 'LiquidThreads' );
 113+ $this->output->addHTML(wfMsg('lqt_threadrequired'));
 114+ return;
 115+ }
 116+ // TODO should implement Threads::withTitle(...).
 117+ $thread = Threads::withRoot( new Article(Title::newFromURL($par)) );
 118+ if (!$thread) {
 119+ wfLoadExtensionMessages( 'LiquidThreads' );
 120+ $this->output->addHTML(wfMsg('lqt_nosuchthread'));
 121+ return;
 122+ }
 123+
 124+ $this->thread = $thread;
 125+
 126+ if ( $this->request->wasPosted() ) {
 127+ $this->handlePost();
 128+ } else {
 129+ $this->handleGet();
 130+ }
 131+
 132+ }
 133+}
Index: trunk/extensions/LiquidThreads/pages/SpecialNewMessages.php
@@ -0,0 +1,55 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class SpecialNewMessages extends SpecialPage {
 7+ private $user, $output, $request, $title;
 8+
 9+ function __construct() {
 10+ SpecialPage::SpecialPage( 'Newmessages' );
 11+ $this->includable( true );
 12+ }
 13+
 14+ /**
 15+ * @see SpecialPage::getDescription
 16+ */
 17+ function getDescription() {
 18+ wfLoadExtensionMessages( 'LiquidThreads' );
 19+ return wfMsg( 'lqt_newmessages' );
 20+ }
 21+
 22+ function execute( $par ) {
 23+ global $wgOut, $wgRequest, $wgTitle, $wgUser;
 24+ wfLoadExtensionMessages( 'LiquidThreads' );
 25+ $this->user = $wgUser;
 26+ $this->output = $wgOut;
 27+ $this->request = $wgRequest;
 28+ $this->title = $wgTitle;
 29+
 30+ $this->setHeaders();
 31+
 32+ $view = new NewUserMessagesView( $this->output, new Article($this->title),
 33+ $this->title, $this->user, $this->request );
 34+
 35+ $view->showOnce(); // handles POST etc.
 36+
 37+ $first_set = NewMessages::newUserMessages($this->user);
 38+ $second_set = NewMessages::watchedThreadsForUser($this->user);
 39+ $both_sets = array_merge($first_set, $second_set);
 40+ if( count($both_sets) == 0 ) {
 41+ $wgOut->addWikitext( wfMsg('lqt-no-new-messages') );
 42+ return;
 43+ }
 44+ $view->showReadAllButton($both_sets); // ugly hack.
 45+
 46+ $view->setHeaderLevel(3);
 47+
 48+ $this->output->addHTML('<h2 class="lqt_newmessages_section">'.wfMsg ( 'lqt-messages-sent' ).'</h2>');
 49+ $view->setThreads( $first_set );
 50+ $view->show();
 51+
 52+ $this->output->addHTML('<h2 class="lqt_newmessages_section">'.wfMsg ( 'lqt-other-messages' ).'</h2>');
 53+ $view->setThreads( $second_set );
 54+ $view->show();
 55+ }
 56+}
Index: trunk/extensions/LiquidThreads/pages/TalkpageView.php
@@ -0,0 +1,223 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class TalkpageView extends LqtView {
 7+ /* Added to SkinTemplateTabs hook in TalkpageView::show(). */
 8+ function customizeTabs( $skintemplate, $content_actions ) {
 9+ // The arguments are passed in by reference.
 10+ unset($content_actions['edit']);
 11+ unset($content_actions['viewsource']);
 12+ unset($content_actions['addsection']);
 13+ unset($content_actions['history']);
 14+ unset($content_actions['watch']);
 15+ unset($content_actions['move']);
 16+
 17+ /*
 18+ TODO:
 19+ We could make these tabs actually follow the tab metaphor if we repointed
 20+ the 'history' and 'edit' tabs to the original subject page. That way 'discussion'
 21+ would just be one of four ways to view the article. But then those other tabs, for
 22+ logged-in users, don't really fit the metaphor. What to do, what to do?
 23+ */
 24+ return true;
 25+ }
 26+
 27+ function permalinksForThreads($ts, $method = null, $operand = null) {
 28+ $ps = array();
 29+ foreach ($ts as $t) {
 30+ $u = $this->permalinkUrl($t, $method, $operand);
 31+ $l = $t->subjectWithoutIncrement();
 32+ $ps[] = "<a href=\"$u\">$l</a>";
 33+ }
 34+ return $ps;
 35+ }
 36+
 37+ function showHeader() {
 38+ /* Show the contents of the actual talkpage article if it exists. */
 39+
 40+ $article = new Article( $this->title );
 41+ $revision = Revision::newFromId($article->getLatest());
 42+ if( $revision ) $article_text = $revision->getRawText();
 43+
 44+ $oldid = $this->request->getVal('oldid', null);
 45+ $editlink = $this->title->getFullURL( 'action=edit' );
 46+
 47+ wfLoadExtensionMessages( 'LiquidThreads' );
 48+ // If $article_text == "", the talkpage was probably just created
 49+ // when the first thread was posted to make the links blue.
 50+ if ( $article->exists() && $article_text != "" ) {
 51+ $historylink = $this->title->getFullURL( 'action=history' );
 52+ $this->openDiv('lqt_header_content');
 53+ $this->showPostBody($article, $oldid);
 54+ $this->outputList('ul', 'lqt_header_commands', null, array(
 55+ "[<a href=\"$editlink\">".wfMsg('edit')."&uarr;</a>]",
 56+ "[<a href=\"$historylink\">".wfMsg('history_short')."&uarr;</a>]"
 57+ ));
 58+ $this->closeDiv();
 59+ } else {
 60+ $this->output->addHTML("<p class=\"lqt_header_notice\">[<a href=\"$editlink\">".wfMsg('lqt_add_header')."</a>]</p>");
 61+ }
 62+ }
 63+
 64+ function outputList( $kind, $class, $id, $contents ) {
 65+ $this->output->addHTML(Xml::openElement($kind, array('class'=>$class,'id'=>$id)));
 66+ foreach ($contents as $li) {
 67+ $this->output->addHTML( Xml::openElement('li') );
 68+ $this->output->addHTML( $li );
 69+ $this->output->addHTML( Xml::closeElement('li') );
 70+ }
 71+ $this->output->addHTML(Xml::closeElement($kind));
 72+ }
 73+
 74+ function showTOC($threads) {
 75+ wfLoadExtensionMessages( 'LiquidThreads' );
 76+
 77+ $sk = $this->user->getSkin();
 78+ $toclines = array();
 79+ $i = 1;
 80+ $toclines[] = $sk->tocIndent();
 81+ foreach($threads as $t) {
 82+ $toclines[] = $sk->tocLine($this->anchorName($t), $t->subjectWithoutIncrement(), $i, 1);
 83+ $i++;
 84+ }
 85+ $toclines[] = $sk->tocUnindent(1);
 86+
 87+ $this->openDiv('lqt_toc_wrapper');
 88+ $this->output->addHTML('<h2 class="lqt_toc_title">'.wfMsg('lqt_contents_title').'</h2> <ul>');
 89+
 90+ foreach($threads as $t) {
 91+ $this->output->addHTML('<li><a href="#'.$this->anchorName($t).'">'.$t->subjectWithoutIncrement().'</a></li>');
 92+ }
 93+
 94+ $this->output->addHTML('</ul></div>');
 95+ }
 96+
 97+ function showArchiveWidget($threads) {
 98+ wfLoadExtensionMessages( 'LiquidThreads' );
 99+
 100+ $threadlinks = $this->permalinksForThreads($threads);
 101+ $url = $this->talkpageUrl($this->title, 'talkpage_archive');
 102+
 103+ if ( count($threadlinks) > 0 ) {
 104+ $this->openDiv('lqt_archive_teaser');
 105+ $this->output->addHTML('<h2 class="lqt_recently_archived">'.wfMsg('lqt_recently_archived').'</h2>');
 106+ // $this->output->addHTML("<span class=\"lqt_browse_archive\">[<a href=\"$url\">".wfMsg('lqt_browse_archive_with_recent')."</a>]</span></h2>");
 107+ $this->outputList('ul', '', '', $threadlinks);
 108+ $this->closeDiv();
 109+ } else {
 110+ }
 111+ }
 112+
 113+ function showTalkpageViewOptions($article) {
 114+ wfLoadExtensionMessages( 'LiquidThreads' );
 115+ // TODO WTF who wrote this?
 116+
 117+ if( $this->methodApplies('talkpage_sort_order') ) {
 118+ $remember_sort_checked = $this->request->getBool('lqt_remember_sort') ? 'checked ' : '';
 119+ $this->user->setOption('lqt_sort_order', $this->sort_order);
 120+ $this->user->saveSettings();
 121+ } else {
 122+ $remember_sort_checked = '';
 123+ }
 124+
 125+ if($article->exists()) {
 126+ $nc_sort = $this->sort_order==LQT_NEWEST_CHANGES ? ' selected' : '';
 127+ $nt_sort = $this->sort_order==LQT_NEWEST_THREADS ? ' selected' : '';
 128+ $ot_sort = $this->sort_order==LQT_OLDEST_THREADS ? ' selected' : '';
 129+ $newest_changes = wfMsg('lqt_sort_newest_changes');
 130+ $newest_threads = wfMsg('lqt_sort_newest_threads');
 131+ $oldest_threads = wfMsg('lqt_sort_oldest_threads');
 132+ $lqt_remember_sort = wfMsg('lqt_remember_sort') ;
 133+ $form_action_url = $this->talkpageUrl( $this->title, 'talkpage_sort_order');
 134+ $lqt_sorting_order = wfMsg('lqt_sorting_order');
 135+ $lqt_sort_newest_changes = wfMsg('lqt_sort_newest_changes');
 136+ $lqt_sort_newest_threads = wfMsg('lqt_sort_newest_threads');
 137+ $lqt_sort_oldest_threads = wfMsg('lqt_sort_oldest_threads');
 138+ $go=wfMsg('go');
 139+ if($this->user->isLoggedIn()) {
 140+ $remember_sort =
 141+ <<<HTML
 142+<br />
 143+<label for="lqt_remember_sort_checkbox">
 144+<input id="lqt_remember_sort_checkbox" name="lqt_remember_sort" type="checkbox" value="1" $remember_sort_checked />
 145+$lqt_remember_sort</label>
 146+HTML;
 147+ } else {
 148+ $remember_sort = '';
 149+ }
 150+ if ( in_array('deletedhistory', $this->user->getRights()) ) {
 151+ $show_deleted_checked = $this->request->getBool('lqt_show_deleted_threads') ? 'checked ' : '';
 152+ $show_deleted = "<br />\n" .
 153+ "<label for=\"lqt_show_deleted_threads_checkbox\">\n" .
 154+ "<input id=\"lqt_show_deleted_threads_checkbox\" name=\"lqt_show_deleted_threads\" type=\"checkbox\" value=\"1\" $show_deleted_checked />\n" .
 155+ wfMsg( 'lqt_delete_show_checkbox' ) . "</label>\n";
 156+ } else {
 157+ $show_deleted = "";
 158+ }
 159+ $this->openDiv('lqt_view_options');
 160+ $this->output->addHTML(
 161+
 162+ <<<HTML
 163+<form name="lqt_sort" action="$form_action_url" method="post">$lqt_sorting_order
 164+<select name="lqt_order" class="lqt_sort_select">
 165+<option value="nc"$nc_sort>$lqt_sort_newest_changes</option>
 166+<option value="nt"$nt_sort>$lqt_sort_newest_threads</option>
 167+<option value="ot"$ot_sort>$lqt_sort_oldest_threads</option>
 168+</select>
 169+$remember_sort
 170+$show_deleted
 171+<input name="submitsort" type="submit" value="$go" class="lqt_go_sort"/>
 172+</form>
 173+HTML
 174+ );
 175+ $this->closeDiv();
 176+ }
 177+
 178+ }
 179+
 180+ function show() {
 181+ global $wgHooks;
 182+ wfLoadExtensionMessages( 'LiquidThreads' );
 183+ // Why is a hook added here?
 184+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 185+
 186+ $this->output->setPageTitle( $this->title->getTalkpage()->getPrefixedText() );
 187+ self::addJSandCSS();
 188+ $article = new Article( $this->title ); // Added in r29715 sorting. Why?
 189+
 190+ // Removed in r29715 sorting. Again, why?
 191+ $this->showHeader();
 192+
 193+ global $wgRequest; // TODO
 194+ if( $this->methodApplies('talkpage_new_thread') ) {
 195+ $this->showNewThreadForm();
 196+ } else {
 197+ $this->showTalkpageViewOptions($article);
 198+ $url = $this->talkpageUrl( $this->title, 'talkpage_new_thread' );
 199+ $this->output->addHTML("<strong><a class=\"lqt_start_discussion\" href=\"$url\">".wfMsg('lqt_new_thread')."</a></strong>");
 200+ }
 201+
 202+ $threads = $this->queries->query('fresh');
 203+
 204+ $this->openDiv('lqt_toc_archive_wrapper');
 205+
 206+ $this->openDiv('lqt_archive_teaser_empty');
 207+ $this->output->addHTML("<div class=\"lqt_browse_archive\"><a href=\"{$this->talkpageUrl($this->title, 'talkpage_archive')}\">".
 208+ wfMsg('lqt_browse_archive_without_recent')."</a></div>");
 209+ $this->closeDiv();
 210+ $recently_archived_threads = $this->queries->query('recently-archived');
 211+ if(count($threads) > 3 || count($recently_archived_threads) > 0) {
 212+ $this->showTOC($threads);
 213+ }
 214+ $this->showArchiveWidget($recently_archived_threads);
 215+ $this->closeDiv();
 216+ // Clear any floats
 217+ $this->output->addHTML('<br clear="all" />');
 218+
 219+ foreach($threads as $t) {
 220+ $this->showThread($t);
 221+ }
 222+ return false;
 223+ }
 224+}
Index: trunk/extensions/LiquidThreads/pages/ThreadHistoricalRevisionView.php
@@ -0,0 +1,44 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class ThreadHistoricalRevisionView extends ThreadPermalinkView {
 7+
 8+ /* TOOD: customize tabs so that History is highlighted. */
 9+
 10+ function postDivClass($thread) {
 11+ $is_changed_thread = $thread->changeObject() &&
 12+ $thread->changeObject()->id() == $thread->id();
 13+ if ( $is_changed_thread )
 14+ return 'lqt_post_changed_by_history';
 15+ else
 16+ return 'lqt_post';
 17+ }
 18+
 19+ function showHistoryInfo() {
 20+ global $wgLang; // TODO global.
 21+ wfLoadExtensionMessages( 'LiquidThreads' );
 22+ $this->openDiv('lqt_history_info');
 23+ $this->output->addHTML(wfMsg('lqt_revision_as_of', $wgLang->timeanddate($this->thread->modified())) .'<br />' );
 24+
 25+ $ct = $this->thread->changeType();
 26+ if( $ct == Threads::CHANGE_NEW_THREAD ) $msg = wfMsg('lqt_change_new_thread');
 27+ else if( $ct == Threads::CHANGE_REPLY_CREATED ) $msg = wfMsg('lqt_change_reply_created');
 28+ else if( $ct == Threads::CHANGE_EDITED_ROOT ) {
 29+ $diff_url = $this->permalinkUrlWithDiff($this->thread);
 30+ $msg = wfMsg('lqt_change_edited_root') . " [<a href=\"$diff_url\">" . wfMsg('diff') . '</a>]';
 31+ }
 32+ $this->output->addHTML($msg);
 33+ $this->closeDiv();
 34+ }
 35+
 36+ function show() {
 37+ if( ! $this->thread ) {
 38+ $this->showMissingThreadPage();
 39+ return false;
 40+ }
 41+ $this->showHistoryInfo();
 42+ parent::show();
 43+ return false;
 44+ }
 45+}
Index: trunk/extensions/LiquidThreads/pages/SummaryPageView.php
@@ -0,0 +1,18 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class SummaryPageView extends LqtView {
 7+ function show() {
 8+ wfLoadExtensionMessages( 'LiquidThreads' );
 9+ $thread = Threads::withSummary($this->article);
 10+ if( $thread ) {
 11+ $url = $thread->root()->getTitle()->getFullURL();
 12+ $name = $thread->root()->getTitle()->getPrefixedText();
 13+ $this->output->setSubtitle(
 14+ wfMsg('lqt_summary_subtitle',
 15+ '<a href="'.$url.'">'.$name.'</a>'));
 16+ }
 17+ return true;
 18+ }
 19+}
Index: trunk/extensions/LiquidThreads/pages/TalkpageArchiveView.php
@@ -0,0 +1,228 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class TalkpageArchiveView extends TalkpageView {
 7+ function __construct(&$output, &$article, &$title, &$user, &$request) {
 8+ parent::__construct($output, $article, $title, $user, $request);
 9+ $this->loadQueryFromRequest();
 10+ }
 11+
 12+ function showThread($t) {
 13+ $this->output->addHTML(<<<HTML
 14+<tr>
 15+<td><a href="{$this->permalinkUrl($t)}">{$t->subjectWithoutIncrement()}</a></td>
 16+<td>
 17+HTML
 18+ ); if( $t->hasSummary() ) {
 19+ $this->showPostBody($t->summary());
 20+ } else if ( $t->type() == Threads::TYPE_MOVED ) {
 21+ $rthread = $t->redirectThread();
 22+ if( $rthread && $rthread->summary() ) {
 23+ $this->showPostBody($rthread->summary());
 24+ }
 25+ }
 26+ $this->output->addHTML(<<<HTML
 27+</td>
 28+</tr>
 29+HTML
 30+ );
 31+ }
 32+
 33+ function loadQueryFromRequest() {
 34+ wfLoadExtensionMessages( 'LiquidThreads' );
 35+ // Begin with with the requirements for being *in* the archive.
 36+ $startdate = Date::now()->nDaysAgo($this->archive_start_days)->midnight();
 37+ $where = array(Threads::articleClause($this->article),
 38+ 'thread.thread_parent is null',
 39+ '(thread.thread_summary_page is not null' .
 40+ ' OR thread.thread_type = '.Threads::TYPE_MOVED.')',
 41+ 'thread.thread_modified < ' . $startdate->text());
 42+ $options = array('ORDER BY thread.thread_modified DESC');
 43+
 44+ $annotations = array( wfMsg ( 'lqt-searching' ));
 45+
 46+ $r = $this->request;
 47+
 48+ /* START AND END DATES */
 49+ // $this->start and $this->end are clipped into the range of available
 50+ // months, for use in the actual query and the selects. $this->raw* are
 51+ // as actually provided, for use by the 'older' and 'newer' buttons.
 52+ $ignore_dates = ! $r->getVal('lqt_archive_filter_by_date', true);
 53+ if ( !$ignore_dates ) {
 54+ $months = Threads::monthsWhereArticleHasThreads($this->article);
 55+ }
 56+ $s = $r->getVal('lqt_archive_start');
 57+ if ($s && ctype_digit($s) && strlen($s) == 6 && !$ignore_dates) {
 58+ $this->selstart = new Date( "{$s}01000000" );
 59+ $this->starti = array_search($s, $months);
 60+ $where[] = 'thread.thread_modified >= ' . $this->selstart->text();
 61+ }
 62+ $e = $r->getVal('lqt_archive_end');
 63+ if ($e && ctype_digit($e) && strlen($e) == 6 && !$ignore_dates) {
 64+ $this->selend = new Date("{$e}01000000");
 65+ $this->endi = array_search($e, $months);
 66+ $where[] = 'thread.thread_modified < ' . $this->selend->nextMonth()->text();
 67+ }
 68+ if ( isset($this->selstart) && isset($this->selend) ) {
 69+
 70+ $this->datespan = $this->starti - $this->endi;
 71+
 72+ $formattedFrom = $this->formattedMonth($this->selstart->text());
 73+ $formattedTo = $this->formattedMonth($this->selend->text());
 74+
 75+ if( $this->datespan == 0 ) {
 76+ $annotations[] = wfMsg('lqt_archive_month_annotation', $formattedFrom);
 77+ } else {
 78+ $annotations[] = wfMsg('lqt_archive_month_range_annotation', $formattedFrom, $formattedTo);
 79+ }
 80+ } else if (isset($this->selstart)) {
 81+ $annotations[] = "after {$this->selstart->text()}";
 82+ } else if (isset($this->selend)) {
 83+ $annotations[] = "before {$this->selend->text()}";
 84+ }
 85+
 86+ $this->where = $where;
 87+ $this->options = $options;
 88+ $this->annotations = implode("<br />\n", $annotations);
 89+ }
 90+
 91+ function threads() {
 92+ return Threads::where($this->where, $this->options);
 93+ }
 94+
 95+ function formattedMonth($yyyymm) {
 96+ global $wgLang; // TODO global.
 97+ return $wgLang->getMonthName( substr($yyyymm, 4, 2) ).' '.substr($yyyymm, 0, 4);
 98+ }
 99+
 100+ function monthSelect($months, $name) {
 101+ $selection = $this->request->getVal($name);
 102+
 103+ // Silently adjust to stay in range.
 104+ $selection = max( min( $selection, $months[0] ), $months[count($months)-1] );
 105+
 106+ $options = array();
 107+ foreach($months as $m) {
 108+ $options[$this->formattedMonth($m)] = $m;
 109+ }
 110+ $result = "<select name=\"$name\" id=\"$name\">";
 111+ foreach( $options as $label => $value ) {
 112+ $selected = $selection == $value ? 'selected="true"' : '';
 113+ $result .= "<option value=\"$value\" $selected>$label";
 114+ }
 115+ $result .= "</select>";
 116+ return $result;
 117+ }
 118+
 119+ function clip( $vals, $min, $max ) {
 120+ $res = array();
 121+ foreach($vals as $val) $res[] = max( min( $val, $max ), $min );
 122+ return $res;
 123+ }
 124+
 125+ /* @return True if there are no threads to show, false otherwise.
 126+ TODO is is somewhat bizarre. */
 127+ function showSearchForm() {
 128+ $months = Threads::monthsWhereArticleHasThreads($this->article);
 129+ if (count($months) == 0) {
 130+ return true;
 131+ }
 132+ wfLoadExtensionMessages( 'LiquidThreads' );
 133+
 134+ $use_dates = $this->request->getVal('lqt_archive_filter_by_date', null);
 135+ if ( $use_dates === null ) {
 136+ $use_dates = $this->request->getBool('lqt_archive_start', false) ||
 137+ $this->request->getBool('lqt_archive_end', false);
 138+ }
 139+ $any_date_check = !$use_dates ? 'checked="1"' : '';
 140+ $these_dates_check = $use_dates ? 'checked="1"' : '';
 141+ $any_date = wfMsg ( 'lqt-any-date' );
 142+ $only_date= wfMsg ( 'lqt-only-date' );
 143+ $date_from= wfMsg ( 'lqt-date-from' );
 144+ $date_to = wfMsg ( 'lqt-date-to' );
 145+ $date_info = wfMsg ( 'lqt-date-info' );
 146+ if( isset($this->datespan) ) {
 147+ $oatte = $this->starti + 1;
 148+ $oatts = $this->starti + 1 + $this->datespan;
 149+
 150+ $natts = $this->endi - 1;
 151+ $natte = $this->endi - 1 - $this->datespan;
 152+
 153+ list($oe, $os, $ns, $ne) =
 154+ $this->clip( array($oatte, $oatts, $natts, $natte),
 155+ 0, count($months)-1 );
 156+
 157+ $older = '<a class="lqt_newer_older" href="' . $this->queryReplace(array(
 158+ 'lqt_archive_filter_by_date'=>'1',
 159+ 'lqt_archive_start' => $months[$os],
 160+ 'lqt_archive_end' => $months[$oe]))
 161+ . '">«'.wfMsg ( 'lqt-older' ).'</a>';
 162+ $newer = '<a class="lqt_newer_older" href="' . $this->queryReplace(array(
 163+ 'lqt_archive_filter_by_date'=>'1',
 164+ 'lqt_archive_start' => $months[$ns],
 165+ 'lqt_archive_end' => $months[$ne]))
 166+ . '">'.wfMsg ( 'lqt-newer' ).'»</a>';
 167+ }
 168+ else {
 169+ $older = '<span class="lqt_newer_older_disabled" title="'.wfMsg ( 'lqt-date-info' ).'">«'.wfMsg ( 'lqt-older' ).'</span>';
 170+ $newer = '<span class="lqt_newer_older_disabled" title="'.wfMsg ( 'lqt-date-info' ).'">'.wfMsg ( 'lqt-newer' ).'»</span>';
 171+ }
 172+
 173+ $this->output->addHTML(<<<HTML
 174+<form id="lqt_archive_search_form" action="{$this->title->getLocalURL()}">
 175+<input type="hidden" name="lqt_method" value="talkpage_archive">
 176+<input type="hidden" name="title" value="{$this->title->getPrefixedURL()}"
 177+
 178+<input type="radio" id="lqt_archive_filter_by_date_no"
 179+name="lqt_archive_filter_by_date" value="0" {$any_date_check}>
 180+<label for="lqt_archive_filter_by_date_no">{$any_date}</label> <br />
 181+<input type="radio" id="lqt_archive_filter_by_date_yes"
 182+name="lqt_archive_filter_by_date" value="1" {$these_dates_check}>
 183+<label for="lqt_archive_filter_by_date_yes">{$only_date}</label> <br />
 184+
 185+<table>
 186+<tr><td><label for="lqt_archive_start">{$date_from}</label>
 187+<td>{$this->monthSelect($months, 'lqt_archive_start')} <br />
 188+<tr><td><label for="lqt_archive_end">{$date_to}</label>
 189+<td>{$this->monthSelect($months, 'lqt_archive_end')}
 190+</table>
 191+<input type="submit">
 192+$older $newer
 193+</form>
 194+HTML
 195+ );
 196+ return false;
 197+ }
 198+
 199+ function show() {
 200+ global $wgHooks;
 201+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 202+
 203+ $this->output->setPageTitle( $this->title->getTalkpage()->getPrefixedText() );
 204+ self::addJSandCSS();
 205+ wfLoadExtensionMessages( 'LiquidThreads' );
 206+
 207+ $empty = $this->showSearchForm();
 208+ if ($empty) {
 209+ $this->output->addHTML('<p><br /><b>'. wfMsg('lqt-nothread' ) . '</b></p>' );
 210+ return false;
 211+ }
 212+ $lqt_title = wfMsg ( 'lqt-title');
 213+ $lqt_summary = wfMsg ( 'lqt-summary' );
 214+ $this->output->addHTML(<<<HTML
 215+<p class="lqt_search_annotations">{$this->annotations}</p>
 216+<table class="lqt_archive_listing">
 217+<col class="lqt_titles" />
 218+<col class="lqt_summaries" />
 219+<tr><th>{$lqt_title}<th>{$lqt_summary}</tr>
 220+HTML
 221+ );
 222+ foreach ($this->threads() as $t) {
 223+ $this->showThread($t);
 224+ }
 225+ $this->output->addHTML('</table>');
 226+
 227+ return false;
 228+ }
 229+}
Index: trunk/extensions/LiquidThreads/pages/ThreadPermalinkView.php
@@ -0,0 +1,123 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class ThreadPermalinkView extends LqtView {
 7+ protected $thread;
 8+
 9+ function customizeTabs( $skintemplate, $content_actions ) {
 10+ wfLoadExtensionMessages( 'LiquidThreads' );
 11+ // Insert fake 'article' and 'discussion' tabs before the thread tab.
 12+ // If you call the key 'talk', the url gets re-set later. TODO:
 13+ // the access key for the talk tab doesn't work.
 14+ $article_t = $this->thread->article()->getTitle();
 15+ $talk_t = $this->thread->article()->getTitle()->getTalkPage();
 16+ efInsertIntoAssoc('article', array(
 17+ 'text'=>wfMsg($article_t->getNamespaceKey()),
 18+ 'href'=>$article_t->getFullURL(),
 19+ 'class' => $article_t->exists() ? '' : 'new'),
 20+ 'nstab-thread', $content_actions);
 21+ efInsertIntoAssoc('not_talk', array(
 22+ // talkpage certainly exists since this thread is from it.
 23+ 'text'=>wfMsg('talk'),
 24+ 'href'=>$talk_t->getFullURL()),
 25+ 'nstab-thread', $content_actions);
 26+
 27+ unset($content_actions['edit']);
 28+ unset($content_actions['viewsource']);
 29+ unset($content_actions['talk']);
 30+ if( array_key_exists( 'move', $content_actions ) && $this->thread ) {
 31+ $content_actions['move']['href'] =
 32+ SpecialPage::getTitleFor('MoveThread')->getFullURL() . '/' .
 33+ $this->thread->title()->getPrefixedURL();
 34+ }
 35+ if( array_key_exists( 'delete', $content_actions ) && $this->thread ) {
 36+ $content_actions['delete']['href'] =
 37+ SpecialPage::getTitleFor('DeleteThread')->getFullURL() . '/' .
 38+ $this->thread->title()->getPrefixedURL();
 39+ }
 40+
 41+ if( array_key_exists('history', $content_actions) ) {
 42+ $content_actions['history']['href'] = $this->permalinkUrl( $this->thread, 'thread_history' );
 43+ if( $this->methodApplies('thread_history') ) {
 44+ $content_actions['history']['class'] = 'selected';
 45+ }
 46+ }
 47+
 48+ return true;
 49+ }
 50+
 51+ function showThreadHeading( $thread ) {
 52+ if ( $this->headerLevel == 2 ) {
 53+ $this->output->setPageTitle( $thread->wikilink() );
 54+ } else {
 55+ parent::showThreadHeading($thread);
 56+ }
 57+ }
 58+
 59+ function noSuchRevision() {
 60+ wfLoadExtensionMessages( 'LiquidThreads' );
 61+ $this->output->addHTML(wfMsg('lqt_nosuchrevision'));
 62+ }
 63+
 64+ function showMissingThreadPage() {
 65+ wfLoadExtensionMessages( 'LiquidThreads' );
 66+ $this->output->addHTML(wfMsg('lqt_nosuchthread'));
 67+ }
 68+
 69+ function getSubtitle() {
 70+ wfLoadExtensionMessages( 'LiquidThreads' );
 71+ // TODO the archive month part is obsolete.
 72+ if (Date::now()->nDaysAgo(30)->midnight()->isBefore( new Date($this->thread->modified()) ))
 73+ $query = '';
 74+ else
 75+ $query = 'lqt_archive_month=' . substr($this->thread->modified(),0,6);
 76+ $talkpage = $this->thread->article()->getTitle()->getTalkpage();
 77+ $talkpage_link = $this->user->getSkin()->makeKnownLinkObj($talkpage, '', $query);
 78+ if ( $this->thread->hasSuperthread() ) {
 79+ return wfMsg('lqt_fragment',"<a href=\"{$this->permalinkUrl($this->thread->topmostThread())}\">".wfMsg('lqt_discussion_link')."</a>",$talkpage_link);
 80+ } else {
 81+ return wfMsg('lqt_from_talk', $talkpage_link);
 82+ }
 83+ }
 84+
 85+ function __construct(&$output, &$article, &$title, &$user, &$request) {
 86+
 87+ parent::__construct($output, $article, $title, $user, $request);
 88+
 89+ $t = Threads::withRoot( $this->article );
 90+ $r = $this->request->getVal('lqt_oldid', null); if( $r ) {
 91+ $t = $t->atRevision($r);
 92+ if( !$t ) { $this->noSuchRevision(); return; }
 93+
 94+ }
 95+ $this->thread = $t;
 96+ if( ! $t ) {
 97+ return; // error reporting is handled in show(). this kinda sucks.
 98+ }
 99+
 100+ // $this->article gets saved to thread_article, so we want it to point to the
 101+ // subject page associated with the talkpage, always, not the permalink url.
 102+ $this->article = $t->article(); # for creating reply threads.
 103+
 104+ }
 105+
 106+ function show() {
 107+ global $wgHooks;
 108+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 109+
 110+ if( ! $this->thread ) {
 111+ $this->showMissingThreadPage();
 112+ return false;
 113+ }
 114+
 115+ self::addJSandCSS();
 116+ $this->output->setSubtitle($this->getSubtitle());
 117+
 118+ if( $this->methodApplies('summarize') )
 119+ $this->showSummarizeForm($this->thread);
 120+
 121+ $this->showThread($this->thread);
 122+ return false;
 123+ }
 124+}
Index: trunk/extensions/LiquidThreads/pages/ThreadWatchView.php
@@ -0,0 +1,11 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die;
 5+
 6+class ThreadWatchView extends ThreadPermalinkView {
 7+ function show() {
 8+ global $wgHooks;
 9+ $wgHooks['SkinTemplateTabs'][] = array($this, 'customizeTabs');
 10+ return true;
 11+ }
 12+}

Status & tagging log