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 @@ |
44 | 44 | $wgHooks['SkinTemplateTabAction'][] = 'LqtDispatch::tabAction'; |
45 | 45 | $wgHooks['OldChangesListRecentChangesLine'][] = 'LqtDispatch::customizeOldChangesList'; |
46 | 46 | $wgHooks['SkinTemplateOutputPageBeforeExec'][] = 'LqtDispatch::setNewtalkHTML'; |
| 47 | +$wgHooks['TitleGetRestrictions'][] = 'Thread::getRestrictionsForTitle'; |
47 | 48 | |
48 | 49 | $wgSpecialPages['DeleteThread'] = 'SpecialDeleteThread'; |
49 | 50 | $wgSpecialPages['MoveThread'] = 'SpecialMoveThread'; |
50 | 51 | $wgSpecialPages['NewMessages'] = 'SpecialNewMessages'; |
51 | 52 | |
52 | | -// Obtained with $ grep -ir 'class .*' *.php | perl -n -e 'if (/(\w+\.php):\s*class (\w+)/) {print "\$wgAutoloadClasses['\''$2'\''] = \$dir.'\''$1'\'';\n";}' |
53 | 53 | $wgAutoloadClasses['LqtDispatch'] = $dir.'LqtBaseView.php'; |
54 | 54 | $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> </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> </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')."↑</a>]", |
| 56 | + "[<a href=\"$historylink\">".wfMsg('history_short')."↑</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 | +} |