Reworked css include/pref/tooltip/accesskey subsystem
[lhc/web/wiklou.git] / includes / Article.php
1 <?php
2 # Class representing a Wikipedia article and history.
3 # See design.doc for an overview.
4
5 # Note: edit user interface and cache support functions have been
6 # moved to separate EditPage and CacheManager classes.
7
8 require_once( 'CacheManager.php' );
9
10 class Article {
11 /* private */ var $mContent, $mContentLoaded;
12 /* private */ var $mUser, $mTimestamp, $mUserText;
13 /* private */ var $mCounter, $mComment, $mCountAdjustment;
14 /* private */ var $mMinorEdit, $mRedirectedFrom;
15 /* private */ var $mTouched, $mFileCache, $mTitle;
16 /* private */ var $mId, $mTable;
17
18 function Article( &$title ) {
19 $this->mTitle =& $title;
20 $this->clear();
21 }
22
23 /* private */ function clear()
24 {
25 $this->mContentLoaded = false;
26 $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded
27 $this->mRedirectedFrom = $this->mUserText =
28 $this->mTimestamp = $this->mComment = $this->mFileCache = '';
29 $this->mCountAdjustment = 0;
30 $this->mTouched = '19700101000000';
31 }
32
33 /* static */ function getRevisionText( $row, $prefix = 'old_' ) {
34 # Deal with optional compression of archived pages.
35 # This can be done periodically via maintenance/compressOld.php, and
36 # as pages are saved if $wgCompressRevisions is set.
37 $text = $prefix . 'text';
38 $flags = $prefix . 'flags';
39 if( isset( $row->$flags ) && (false !== strpos( $row->$flags, 'gzip' ) ) ) {
40 return gzinflate( $row->$text );
41 }
42 if( isset( $row->$text ) ) {
43 return $row->$text;
44 }
45 return false;
46 }
47
48 /* static */ function compressRevisionText( &$text ) {
49 global $wgCompressRevisions;
50 if( !$wgCompressRevisions ) {
51 return '';
52 }
53 if( !function_exists( 'gzdeflate' ) ) {
54 wfDebug( "Article::compressRevisionText() -- no zlib support, not compressing\n" );
55 return '';
56 }
57 $text = gzdeflate( $text );
58 return 'gzip';
59 }
60
61 # Note that getContent/loadContent may follow redirects if
62 # not told otherwise, and so may cause a change to mTitle.
63
64 # Return the text of this revision
65 function getContent( $noredir )
66 {
67 global $wgRequest;
68
69 # Get variables from query string :P
70 $action = $wgRequest->getText( 'action', 'view' );
71 $section = $wgRequest->getText( 'section' );
72
73 $fname = 'Article::getContent';
74 wfProfileIn( $fname );
75
76 if ( 0 == $this->getID() ) {
77 if ( 'edit' == $action ) {
78 wfProfileOut( $fname );
79 return ''; # was "newarticletext", now moved above the box)
80 }
81 wfProfileOut( $fname );
82 return wfMsg( 'noarticletext' );
83 } else {
84 $this->loadContent( $noredir );
85
86 if(
87 # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
88 ( $this->mTitle->getNamespace() == Namespace::getTalk( Namespace::getUser()) ) &&
89 preg_match('/^\d{1,3}\.\d{1,3}.\d{1,3}\.\d{1,3}$/',$this->mTitle->getText()) &&
90 $action=='view'
91 )
92 {
93 wfProfileOut( $fname );
94 return $this->mContent . "\n" .wfMsg('anontalkpagetext'); }
95 else {
96 if($action=='edit') {
97 if($section!='') {
98 if($section=='new') {
99 wfProfileOut( $fname );
100 return '';
101 }
102
103 # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
104 # comments to be stripped as well)
105 $rv=$this->getSection($this->mContent,$section);
106 wfProfileOut( $fname );
107 return $rv;
108 }
109 }
110 wfProfileOut( $fname );
111 return $this->mContent;
112 }
113 }
114 }
115
116 # This function returns the text of a section, specified by a number ($section).
117 # A section is text under a heading like == Heading == or <h1>Heading</h1>, or
118 # the first section before any such heading (section 0).
119 #
120 # If a section contains subsections, these are also returned.
121 #
122 function getSection($text,$section) {
123
124 # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
125 # comments to be stripped as well)
126 $striparray=array();
127 $parser=new Parser();
128 $parser->mOutputType=OT_WIKI;
129 $striptext=$parser->strip($text, $striparray, true);
130
131 # now that we can be sure that no pseudo-sections are in the source,
132 # split it up by section
133 $secs =
134 preg_split(
135 '/(^=+.*?=+|^<h[1-6].*?' . '>.*?<\/h[1-6].*?' . '>)/mi',
136 $striptext, -1,
137 PREG_SPLIT_DELIM_CAPTURE);
138 if($section==0) {
139 $rv=$secs[0];
140 } else {
141 $headline=$secs[$section*2-1];
142 preg_match( '/^(=+).*?=+|^<h([1-6]).*?' . '>.*?<\/h[1-6].*?' . '>/mi',$headline,$matches);
143 $hlevel=$matches[1];
144
145 # translate wiki heading into level
146 if(strpos($hlevel,'=')!==false) {
147 $hlevel=strlen($hlevel);
148 }
149
150 $rv=$headline. $secs[$section*2];
151 $count=$section+1;
152
153 $break=false;
154 while(!empty($secs[$count*2-1]) && !$break) {
155
156 $subheadline=$secs[$count*2-1];
157 preg_match( '/^(=+).*?=+|^<h([1-6]).*?' . '>.*?<\/h[1-6].*?' . '>/mi',$subheadline,$matches);
158 $subhlevel=$matches[1];
159 if(strpos($subhlevel,'=')!==false) {
160 $subhlevel=strlen($subhlevel);
161 }
162 if($subhlevel > $hlevel) {
163 $rv.=$subheadline.$secs[$count*2];
164 }
165 if($subhlevel <= $hlevel) {
166 $break=true;
167 }
168 $count++;
169
170 }
171 }
172 # reinsert stripped tags
173 $rv=$parser->unstrip($rv,$striparray);
174 $rv=$parser->unstripNoWiki($rv,$striparray);
175 $rv=trim($rv);
176 return $rv;
177
178 }
179
180
181 # Load the revision (including cur_text) into this object
182 function loadContent( $noredir = false )
183 {
184 global $wgOut, $wgMwRedir, $wgRequest;
185
186 # Query variables :P
187 $oldid = $wgRequest->getVal( 'oldid' );
188 $redirect = $wgRequest->getVal( 'redirect' );
189
190 if ( $this->mContentLoaded ) return;
191 $fname = 'Article::loadContent';
192
193 # Pre-fill content with error message so that if something
194 # fails we'll have something telling us what we intended.
195
196 $t = $this->mTitle->getPrefixedText();
197 if ( isset( $oldid ) ) {
198 $oldid = IntVal( $oldid );
199 $t .= ",oldid={$oldid}";
200 }
201 if ( isset( $redirect ) ) {
202 $redirect = ($redirect == 'no') ? 'no' : 'yes';
203 $t .= ",redirect={$redirect}";
204 }
205 $this->mContent = wfMsg( 'missingarticle', $t );
206
207 if ( ! $oldid ) { # Retrieve current version
208 $id = $this->getID();
209 if ( 0 == $id ) return;
210
211 $sql = 'SELECT ' .
212 'cur_text,cur_timestamp,cur_user,cur_user_text,cur_comment,cur_counter,cur_restrictions,cur_touched ' .
213 "FROM cur WHERE cur_id={$id}";
214 wfDebug( "$sql\n" );
215 $res = wfQuery( $sql, DB_READ, $fname );
216 if ( 0 == wfNumRows( $res ) ) {
217 return;
218 }
219
220 $s = wfFetchObject( $res );
221 # If we got a redirect, follow it (unless we've been told
222 # not to by either the function parameter or the query
223 if ( ( 'no' != $redirect ) && ( false == $noredir ) &&
224 ( $wgMwRedir->matchStart( $s->cur_text ) ) ) {
225 if ( preg_match( '/\\[\\[([^\\]\\|]+)[\\]\\|]/',
226 $s->cur_text, $m ) ) {
227 $rt = Title::newFromText( $m[1] );
228 if( $rt ) {
229 # Gotta hand redirects to special pages differently:
230 # Fill the HTTP response "Location" header and ignore
231 # the rest of the page we're on.
232
233 if ( $rt->getInterwiki() != '' ) {
234 $wgOut->redirect( $rt->getFullURL() ) ;
235 return;
236 }
237 if ( $rt->getNamespace() == Namespace::getSpecial() ) {
238 $wgOut->redirect( $rt->getFullURL() );
239 return;
240 }
241 $rid = $rt->getArticleID();
242 if ( 0 != $rid ) {
243 $sql = 'SELECT cur_text,cur_timestamp,cur_user,cur_user_text,cur_comment,' .
244 "cur_counter,cur_restrictions,cur_touched FROM cur WHERE cur_id={$rid}";
245 $res = wfQuery( $sql, DB_READ, $fname );
246
247 if ( 0 != wfNumRows( $res ) ) {
248 $this->mRedirectedFrom = $this->mTitle->getPrefixedText();
249 $this->mTitle = $rt;
250 $s = wfFetchObject( $res );
251 }
252 }
253 }
254 }
255 }
256
257 $this->mContent = $s->cur_text;
258 $this->mUser = $s->cur_user;
259 $this->mUserText = $s->cur_user_text;
260 $this->mComment = $s->cur_comment;
261 $this->mCounter = $s->cur_counter;
262 $this->mTimestamp = $s->cur_timestamp;
263 $this->mTouched = $s->cur_touched;
264 $this->mTitle->mRestrictions = explode( ',', trim( $s->cur_restrictions ) );
265 $this->mTitle->mRestrictionsLoaded = true;
266 wfFreeResult( $res );
267 } else { # oldid set, retrieve historical version
268 $sql = 'SELECT old_namespace,old_title,old_text,old_timestamp,old_user,old_user_text,old_comment,old_flags FROM old ' .
269 "WHERE old_id={$oldid}";
270 $res = wfQuery( $sql, DB_READ, $fname );
271 if ( 0 == wfNumRows( $res ) ) {
272 return;
273 }
274
275 $s = wfFetchObject( $res );
276 if( $this->mTitle->getNamespace() != $s->old_namespace ||
277 $this->mTitle->getDBkey() != $s->old_title ) {
278 $oldTitle = Title::makeTitle( $s->old_namesapce, $s->old_title );
279 $this->mTitle = $oldTitle;
280 $wgTitle = $oldTitle;
281 }
282 $this->mContent = Article::getRevisionText( $s );
283 $this->mUser = $s->old_user;
284 $this->mUserText = $s->old_user_text;
285 $this->mComment = $s->old_comment;
286 $this->mCounter = 0;
287 $this->mTimestamp = $s->old_timestamp;
288 wfFreeResult( $res );
289 }
290 $this->mContentLoaded = true;
291 return $this->mContent;
292 }
293
294 # Gets the article text without using so many damn globals
295 # Returns false on error
296 function getContentWithoutUsingSoManyDamnGlobals( $oldid = 0, $noredir = false ) {
297 global $wgMwRedir;
298
299 if ( $this->mContentLoaded ) {
300 return $this->mContent;
301 }
302 $this->mContent = false;
303
304 $fname = 'Article::loadContent';
305
306 if ( ! $oldid ) { # Retrieve current version
307 $id = $this->getID();
308 if ( 0 == $id ) {
309 return false;
310 }
311
312 $sql = 'SELECT ' .
313 'cur_text,cur_timestamp,cur_user,cur_counter,cur_restrictions,cur_touched ' .
314 "FROM cur WHERE cur_id={$id}";
315 $res = wfQuery( $sql, DB_READ, $fname );
316 if ( 0 == wfNumRows( $res ) ) {
317 return false;
318 }
319
320 $s = wfFetchObject( $res );
321 # If we got a redirect, follow it (unless we've been told
322 # not to by either the function parameter or the query
323 if ( !$noredir && $wgMwRedir->matchStart( $s->cur_text ) ) {
324 if ( preg_match( '/\\[\\[([^\\]\\|]+)[\\]\\|]/',
325 $s->cur_text, $m ) ) {
326 $rt = Title::newFromText( $m[1] );
327 if( $rt && $rt->getInterwiki() == '' && $rt->getNamespace() != Namespace::getSpecial() ) {
328 $rid = $rt->getArticleID();
329 if ( 0 != $rid ) {
330 $sql = 'SELECT cur_text,cur_timestamp,cur_user,' .
331 "cur_counter,cur_restrictions,cur_touched FROM cur WHERE cur_id={$rid}";
332 $res = wfQuery( $sql, DB_READ, $fname );
333
334 if ( 0 != wfNumRows( $res ) ) {
335 $this->mRedirectedFrom = $this->mTitle->getPrefixedText();
336 $this->mTitle = $rt;
337 $s = wfFetchObject( $res );
338 }
339 }
340 }
341 }
342 }
343
344 $this->mContent = $s->cur_text;
345 $this->mUser = $s->cur_user;
346 $this->mCounter = $s->cur_counter;
347 $this->mTimestamp = $s->cur_timestamp;
348 $this->mTouched = $s->cur_touched;
349 $this->mTitle->mRestrictions = explode( ",", trim( $s->cur_restrictions ) );
350 $this->mTitle->mRestrictionsLoaded = true;
351 wfFreeResult( $res );
352 } else { # oldid set, retrieve historical version
353 $sql = 'SELECT old_text,old_timestamp,old_user,old_flags FROM old ' .
354 "WHERE old_id={$oldid}";
355 $res = wfQuery( $sql, DB_READ, $fname );
356 if ( 0 == wfNumRows( $res ) ) {
357 return false;
358 }
359
360 $s = wfFetchObject( $res );
361 $this->mContent = Article::getRevisionText( $s );
362 $this->mUser = $s->old_user;
363 $this->mCounter = 0;
364 $this->mTimestamp = $s->old_timestamp;
365 wfFreeResult( $res );
366 }
367 $this->mContentLoaded = true;
368 return $this->mContent;
369 }
370
371 function getID() {
372 if( $this->mTitle ) {
373 return $this->mTitle->getArticleID();
374 } else {
375 return 0;
376 }
377 }
378
379 function getCount()
380 {
381 if ( -1 == $this->mCounter ) {
382 $id = $this->getID();
383 $this->mCounter = wfGetSQL( 'cur', 'cur_counter', "cur_id={$id}" );
384 }
385 return $this->mCounter;
386 }
387
388 # Would the given text make this article a "good" article (i.e.,
389 # suitable for including in the article count)?
390
391 function isCountable( $text )
392 {
393 global $wgUseCommaCount, $wgMwRedir;
394
395 if ( 0 != $this->mTitle->getNamespace() ) { return 0; }
396 if ( $wgMwRedir->matchStart( $text ) ) { return 0; }
397 $token = ($wgUseCommaCount ? ',' : '[[' );
398 if ( false === strstr( $text, $token ) ) { return 0; }
399 return 1;
400 }
401
402 # Loads everything from cur except cur_text
403 # This isn't necessary for all uses, so it's only done if needed.
404
405 /* private */ function loadLastEdit()
406 {
407 global $wgOut;
408 if ( -1 != $this->mUser ) return;
409
410 $sql = 'SELECT cur_user,cur_user_text,cur_timestamp,' .
411 'cur_comment,cur_minor_edit FROM cur WHERE ' .
412 'cur_id=' . $this->getID();
413 $res = wfQuery( $sql, DB_READ, 'Article::loadLastEdit' );
414
415 if ( wfNumRows( $res ) > 0 ) {
416 $s = wfFetchObject( $res );
417 $this->mUser = $s->cur_user;
418 $this->mUserText = $s->cur_user_text;
419 $this->mTimestamp = $s->cur_timestamp;
420 $this->mComment = $s->cur_comment;
421 $this->mMinorEdit = $s->cur_minor_edit;
422 }
423 }
424
425 function getTimestamp()
426 {
427 $this->loadLastEdit();
428 return $this->mTimestamp;
429 }
430
431 function getUser()
432 {
433 $this->loadLastEdit();
434 return $this->mUser;
435 }
436
437 function getUserText()
438 {
439 $this->loadLastEdit();
440 return $this->mUserText;
441 }
442
443 function getComment()
444 {
445 $this->loadLastEdit();
446 return $this->mComment;
447 }
448
449 function getMinorEdit()
450 {
451 $this->loadLastEdit();
452 return $this->mMinorEdit;
453 }
454
455 function getContributors($limit = 0, $offset = 0)
456 {
457 $fname = 'Article::getContributors';
458
459 # XXX: this is expensive; cache this info somewhere.
460
461 $title = $this->mTitle;
462
463 $contribs = array();
464
465 $sql = 'SELECT old.old_user, old.old_user_text, ' .
466 ' user.user_real_name, MAX(old.old_timestamp) as timestamp' .
467 ' FROM old, user ' .
468 ' WHERE old.old_user = user.user_id ' .
469 ' AND old.old_namespace = ' . $title->getNamespace() .
470 ' AND old.old_title = "' . $title->getDBkey() . '"' .
471 ' AND old.old_user != 0 ' .
472 ' AND old.old_user != ' . $this->getUser() .
473 ' GROUP BY old.old_user ' .
474 ' ORDER BY timestamp DESC ';
475
476 if ($limit > 0) {
477 $sql .= ' LIMIT '.$limit;
478 }
479
480 $res = wfQuery($sql, DB_READ, $fname);
481
482 while ( $line = wfFetchObject( $res ) ) {
483 $contribs[$line->old_user] =
484 array($line->old_user_text, $line->user_real_name);
485 }
486
487 # Count anonymous users
488
489 $res = wfQuery('SELECT COUNT(*) AS cnt ' .
490 ' FROM old ' .
491 ' WHERE old_namespace = ' . $title->getNamespace() .
492 " AND old_title = '" . $title->getDBkey() . "'" .
493 ' AND old_user = 0 ', DB_READ, $fname);
494
495 while ( $line = wfFetchObject( $res ) ) {
496 $contribs[0] = array($line->cnt, 'Anonymous');
497 }
498
499 return $contribs;
500 }
501
502 # This is the default action of the script: just view the page of
503 # the given title.
504
505 function view()
506 {
507 global $wgUser, $wgOut, $wgLang, $wgRequest;
508 global $wgLinkCache, $IP, $wgEnableParserCache;
509
510 $fname = 'Article::view';
511 wfProfileIn( $fname );
512
513 # Get variables from query string :P
514 $oldid = $wgRequest->getVal( 'oldid' );
515 $diff = $wgRequest->getVal( 'diff' );
516
517 $wgOut->setArticleFlag( true );
518 $wgOut->setRobotpolicy( 'index,follow' );
519
520 # If we got diff and oldid in the query, we want to see a
521 # diff page instead of the article.
522
523 if ( !is_null( $diff ) ) {
524 $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
525 $de = new DifferenceEngine( intval($oldid), intval($diff) );
526 $de->showDiffPage();
527 wfProfileOut( $fname );
528 return;
529 }
530
531 if ( !is_null( $oldid ) and $this->checkTouched() ) {
532 if( $wgOut->checkLastModified( $this->mTouched ) ){
533 return;
534 } else if ( $this->tryFileCache() ) {
535 # tell wgOut that output is taken care of
536 $wgOut->disable();
537 $this->viewUpdates();
538 return;
539 }
540 }
541
542 # Should the parser cache be used?
543 if ( $wgEnableParserCache && intval($wgUser->getOption( 'stubthreshold' )) == 0 && empty( $oldid ) ) {
544 $pcache = true;
545 } else {
546 $pcache = false;
547 }
548
549 $outputDone = false;
550 if ( $pcache ) {
551 if ( $wgOut->tryParserCache( $this, $wgUser ) ) {
552 $outputDone = true;
553 }
554 }
555
556 if ( !$outputDone ) {
557 $text = $this->getContent( false ); # May change mTitle by following a redirect
558
559 # Another whitelist check in case oldid or redirects are altering the title
560 if ( !$this->mTitle->userCanRead() ) {
561 $wgOut->loginToUse();
562 $wgOut->output();
563 exit;
564 }
565
566
567 # We're looking at an old revision
568
569 if ( !empty( $oldid ) ) {
570 $this->setOldSubtitle();
571 $wgOut->setRobotpolicy( 'noindex,follow' );
572 }
573 if ( '' != $this->mRedirectedFrom ) {
574 $sk = $wgUser->getSkin();
575 $redir = $sk->makeKnownLink( $this->mRedirectedFrom, '',
576 'redirect=no' );
577 $s = wfMsg( 'redirectedfrom', $redir );
578 $wgOut->setSubtitle( $s );
579
580 # Can't cache redirects
581 $pcache = false;
582 }
583
584 $wgLinkCache->preFill( $this->mTitle );
585
586 # wrap user css and user js in pre and don't parse
587 # XXX: use $this->mTitle->usCssJsSubpage() when php is fixed/ a workaround is found
588 if (
589 $this->mTitle->getNamespace() == Namespace::getUser() &&
590 preg_match('/\\/[\\w]+\\.(css|js)$/', $this->mTitle->getDBkey())
591 ) {
592 $wgOut->addWikiText( wfMsg('clearyourcache'));
593 $wgOut->addHTML( '<pre>'.htmlspecialchars($this->mContent)."\n</pre>" );
594 } else if ( $pcache ) {
595 $wgOut->addWikiText( $text, true, $this );
596 } else {
597 $wgOut->addWikiText( $text );
598 }
599 }
600 $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
601
602 # Add link titles as META keywords
603 $wgOut->addMetaTags() ;
604
605 $this->viewUpdates();
606 wfProfileOut( $fname );
607 }
608
609 # Theoretically we could defer these whole insert and update
610 # functions for after display, but that's taking a big leap
611 # of faith, and we want to be able to report database
612 # errors at some point.
613
614 /* private */ function insertNewArticle( $text, $summary, $isminor, $watchthis )
615 {
616 global $wgOut, $wgUser, $wgLinkCache, $wgMwRedir;
617 global $wgUseSquid, $wgDeferredUpdateList, $wgInternalServer;
618
619 $fname = 'Article::insertNewArticle';
620
621 $this->mCountAdjustment = $this->isCountable( $text );
622
623 $ns = $this->mTitle->getNamespace();
624 $ttl = $this->mTitle->getDBkey();
625 $text = $this->preSaveTransform( $text );
626 if ( $wgMwRedir->matchStart( $text ) ) { $redir = 1; }
627 else { $redir = 0; }
628
629 $now = wfTimestampNow();
630 $won = wfInvertTimestamp( $now );
631 wfSeedRandom();
632 $rand = number_format( mt_rand() / mt_getrandmax(), 12, '.', '' );
633 $isminor = ( $isminor && $wgUser->getID() ) ? 1 : 0;
634 $sql = 'INSERT INTO cur (cur_namespace,cur_title,cur_text,' .
635 'cur_comment,cur_user,cur_timestamp,cur_minor_edit,cur_counter,' .
636 'cur_restrictions,cur_user_text,cur_is_redirect,' .
637 "cur_is_new,cur_random,cur_touched,inverse_timestamp) VALUES ({$ns},'" . wfStrencode( $ttl ) . "', '" .
638 wfStrencode( $text ) . "', '" .
639 wfStrencode( $summary ) . "', '" .
640 $wgUser->getID() . "', '{$now}', " .
641 $isminor . ", 0, '', '" .
642 wfStrencode( $wgUser->getName() ) . "', $redir, 1, $rand, '{$now}', '{$won}')";
643 $res = wfQuery( $sql, DB_WRITE, $fname );
644
645 $newid = wfInsertId();
646 $this->mTitle->resetArticleID( $newid );
647
648 Article::onArticleCreate( $this->mTitle );
649 RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary );
650
651 if ($watchthis) {
652 if(!$this->mTitle->userIsWatching()) $this->watch();
653 } else {
654 if ( $this->mTitle->userIsWatching() ) {
655 $this->unwatch();
656 }
657 }
658
659 # The talk page isn't in the regular link tables, so we need to update manually:
660 $talkns = $ns ^ 1; # talk -> normal; normal -> talk
661 $sql = "UPDATE cur set cur_touched='$now' WHERE cur_namespace=$talkns AND cur_title='" . wfStrencode( $ttl ) . "'";
662 wfQuery( $sql, DB_WRITE );
663
664 # standard deferred updates
665 $this->editUpdates( $text );
666
667 $this->showArticle( $text, wfMsg( 'newarticle' ) );
668 }
669
670
671 /* Side effects: loads last edit */
672 function getTextOfLastEditWithSectionReplacedOrAdded($section, $text, $summary = ''){
673 $this->loadLastEdit();
674 $oldtext = $this->getContent( true );
675 if ($section != '') {
676 if($section=='new') {
677 if($summary) $subject="== {$summary} ==\n\n";
678 $text=$oldtext."\n\n".$subject.$text;
679 } else {
680
681 # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
682 # comments to be stripped as well)
683 $striparray=array();
684 $parser=new Parser();
685 $parser->mOutputType=OT_WIKI;
686 $oldtext=$parser->strip($oldtext, $striparray, true);
687
688 # now that we can be sure that no pseudo-sections are in the source,
689 # split it up
690 # Unfortunately we can't simply do a preg_replace because that might
691 # replace the wrong section, so we have to use the section counter instead
692 $secs=preg_split('/(^=+.*?=+|^<h[1-6].*?' . '>.*?<\/h[1-6].*?' . '>)/mi',
693 $oldtext,-1,PREG_SPLIT_DELIM_CAPTURE);
694 $secs[$section*2]=$text."\n\n"; // replace with edited
695
696 # section 0 is top (intro) section
697 if($section!=0) {
698
699 # headline of old section - we need to go through this section
700 # to determine if there are any subsections that now need to
701 # be erased, as the mother section has been replaced with
702 # the text of all subsections.
703 $headline=$secs[$section*2-1];
704 preg_match( '/^(=+).*?=+|^<h([1-6]).*?' . '>.*?<\/h[1-6].*?' . '>/mi',$headline,$matches);
705 $hlevel=$matches[1];
706
707 # determine headline level for wikimarkup headings
708 if(strpos($hlevel,'=')!==false) {
709 $hlevel=strlen($hlevel);
710 }
711
712 $secs[$section*2-1]=''; // erase old headline
713 $count=$section+1;
714 $break=false;
715 while(!empty($secs[$count*2-1]) && !$break) {
716
717 $subheadline=$secs[$count*2-1];
718 preg_match(
719 '/^(=+).*?=+|^<h([1-6]).*?' . '>.*?<\/h[1-6].*?' . '>/mi',$subheadline,$matches);
720 $subhlevel=$matches[1];
721 if(strpos($subhlevel,'=')!==false) {
722 $subhlevel=strlen($subhlevel);
723 }
724 if($subhlevel > $hlevel) {
725 // erase old subsections
726 $secs[$count*2-1]='';
727 $secs[$count*2]='';
728 }
729 if($subhlevel <= $hlevel) {
730 $break=true;
731 }
732 $count++;
733
734 }
735
736 }
737 $text=join('',$secs);
738 # reinsert the stuff that we stripped out earlier
739 $text=$parser->unstrip($text,$striparray);
740 $text=$parser->unstripNoWiki($text,$striparray);
741 }
742
743 }
744 return $text;
745 }
746
747 function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' )
748 {
749 global $wgOut, $wgUser, $wgLinkCache;
750 global $wgDBtransactions, $wgMwRedir;
751 global $wgUseSquid, $wgInternalServer;
752 $fname = 'Article::updateArticle';
753
754 if ( $this->mMinorEdit ) { $me1 = 1; } else { $me1 = 0; }
755 if ( $minor && $wgUser->getID() ) { $me2 = 1; } else { $me2 = 0; }
756 if ( preg_match( "/^((" . $wgMwRedir->getBaseRegex() . ')[^\\n]+)/i', $text, $m ) ) {
757 $redir = 1;
758 $text = $m[1] . "\n"; # Remove all content but redirect
759 }
760 else { $redir = 0; }
761
762 $text = $this->preSaveTransform( $text );
763
764 # Update article, but only if changed.
765
766 if( $wgDBtransactions ) {
767 $sql = 'BEGIN';
768 wfQuery( $sql, DB_WRITE );
769 }
770 $oldtext = $this->getContent( true );
771
772 if ( 0 != strcmp( $text, $oldtext ) ) {
773 $this->mCountAdjustment = $this->isCountable( $text )
774 - $this->isCountable( $oldtext );
775
776 $now = wfTimestampNow();
777 $won = wfInvertTimestamp( $now );
778 $sql = "UPDATE cur SET cur_text='" . wfStrencode( $text ) .
779 "',cur_comment='" . wfStrencode( $summary ) .
780 "',cur_minor_edit={$me2}, cur_user=" . $wgUser->getID() .
781 ",cur_timestamp='{$now}',cur_user_text='" .
782 wfStrencode( $wgUser->getName() ) .
783 "',cur_is_redirect={$redir}, cur_is_new=0, cur_touched='{$now}', inverse_timestamp='{$won}' " .
784 "WHERE cur_id=" . $this->getID() .
785 " AND cur_timestamp='" . $this->getTimestamp() . "'";
786 $res = wfQuery( $sql, DB_WRITE, $fname );
787
788 if( wfAffectedRows() == 0 ) {
789 /* Belated edit conflict! Run away!! */
790 return false;
791 }
792
793 # This overwrites $oldtext if revision compression is on
794 $flags = Article::compressRevisionText( $oldtext );
795
796 $sql = 'INSERT INTO old (old_namespace,old_title,old_text,' .
797 'old_comment,old_user,old_user_text,old_timestamp,' .
798 'old_minor_edit,inverse_timestamp,old_flags) VALUES (' .
799 $this->mTitle->getNamespace() . ", '" .
800 wfStrencode( $this->mTitle->getDBkey() ) . "', '" .
801 wfStrencode( $oldtext ) . "', '" .
802 wfStrencode( $this->getComment() ) . "', " .
803 $this->getUser() . ", '" .
804 wfStrencode( $this->getUserText() ) . "', '" .
805 $this->getTimestamp() . "', " . $me1 . ", '" .
806 wfInvertTimestamp( $this->getTimestamp() ) . "','$flags')";
807 $res = wfQuery( $sql, DB_WRITE, $fname );
808 $oldid = wfInsertID( $res );
809
810 $bot = (int)($wgUser->isBot() || $forceBot);
811 RecentChange::notifyEdit( $now, $this->mTitle, $me2, $wgUser, $summary,
812 $oldid, $this->getTimestamp(), $bot );
813 Article::onArticleEdit( $this->mTitle );
814 }
815
816 if( $wgDBtransactions ) {
817 $sql = 'COMMIT';
818 wfQuery( $sql, DB_WRITE );
819 }
820
821 if ($watchthis) {
822 if (!$this->mTitle->userIsWatching()) $this->watch();
823 } else {
824 if ( $this->mTitle->userIsWatching() ) {
825 $this->unwatch();
826 }
827 }
828 # standard deferred updates
829 $this->editUpdates( $text );
830
831
832 $urls = array();
833 # Template namespace
834 # Purge all articles linking here
835 if ( $this->mTitle->getNamespace() == NS_TEMPLATE) {
836 $titles = $this->mTitle->getLinksTo();
837 Title::touchArray( $titles );
838 if ( $wgUseSquid ) {
839 foreach ( $titles as $title ) {
840 $urls[] = $title->getInternalURL();
841 }
842 }
843 }
844
845 # Squid updates
846 if ( $wgUseSquid ) {
847 $urls = array_merge( $urls, $this->mTitle->getSquidURLs() );
848 $u = new SquidUpdate( $urls );
849 $u->doUpdate();
850 }
851
852 $this->showArticle( $text, wfMsg( 'updated' ), $sectionanchor );
853 return true;
854 }
855
856 # After we've either updated or inserted the article, update
857 # the link tables and redirect to the new page.
858
859 function showArticle( $text, $subtitle , $sectionanchor = '' )
860 {
861 global $wgOut, $wgUser, $wgLinkCache;
862 global $wgMwRedir;
863
864 $wgLinkCache = new LinkCache();
865
866 # Get old version of link table to allow incremental link updates
867 $wgLinkCache->preFill( $this->mTitle );
868 $wgLinkCache->clear();
869
870 # Now update the link cache by parsing the text
871 $wgOut = new OutputPage();
872 $wgOut->addWikiText( $text );
873
874 if( $wgMwRedir->matchStart( $text ) )
875 $r = 'redirect=no';
876 else
877 $r = '';
878 $wgOut->redirect( $this->mTitle->getFullURL( $r ).$sectionanchor );
879 }
880
881 # Add this page to my watchlist
882
883 function watch( $add = true )
884 {
885 global $wgUser, $wgOut, $wgLang;
886 global $wgDeferredUpdateList;
887
888 if ( 0 == $wgUser->getID() ) {
889 $wgOut->errorpage( 'watchnologin', 'watchnologintext' );
890 return;
891 }
892 if ( wfReadOnly() ) {
893 $wgOut->readOnlyPage();
894 return;
895 }
896 if( $add )
897 $wgUser->addWatch( $this->mTitle );
898 else
899 $wgUser->removeWatch( $this->mTitle );
900
901 $wgOut->setPagetitle( wfMsg( $add ? 'addedwatch' : 'removedwatch' ) );
902 $wgOut->setRobotpolicy( 'noindex,follow' );
903
904 $sk = $wgUser->getSkin() ;
905 $link = $this->mTitle->getPrefixedText();
906
907 if($add)
908 $text = wfMsg( 'addedwatchtext', $link );
909 else
910 $text = wfMsg( 'removedwatchtext', $link );
911 $wgOut->addWikiText( $text );
912
913 $up = new UserUpdate();
914 array_push( $wgDeferredUpdateList, $up );
915
916 $wgOut->returnToMain( false );
917 }
918
919 function unwatch()
920 {
921 $this->watch( false );
922 }
923
924 function protect( $limit = 'sysop' )
925 {
926 global $wgUser, $wgOut, $wgRequest;
927
928 if ( ! $wgUser->isSysop() ) {
929 $wgOut->sysopRequired();
930 return;
931 }
932 if ( wfReadOnly() ) {
933 $wgOut->readOnlyPage();
934 return;
935 }
936 $id = $this->mTitle->getArticleID();
937 if ( 0 == $id ) {
938 $wgOut->fatalError( wfMsg( 'badarticleerror' ) );
939 return;
940 }
941
942 $confirm = $wgRequest->getBool( 'wpConfirmProtect' ) && $wgRequest->wasPosted();
943 $reason = $wgRequest->getText( 'wpReasonProtect' );
944
945 if ( $confirm ) {
946
947 $sql = "UPDATE cur SET cur_touched='" . wfTimestampNow() . "'," .
948 "cur_restrictions='{$limit}' WHERE cur_id={$id}";
949 wfQuery( $sql, DB_WRITE, 'Article::protect' );
950
951 $log = new LogPage( wfMsg( 'protectlogpage' ), wfMsg( 'protectlogtext' ) );
952 if ( $limit === "" ) {
953 $log->addEntry( wfMsg( 'unprotectedarticle', $this->mTitle->getPrefixedText() ), $reason );
954 } else {
955 $log->addEntry( wfMsg( 'protectedarticle', $this->mTitle->getPrefixedText() ), $reason );
956 }
957 $wgOut->redirect( $this->mTitle->getFullURL() );
958 return;
959 } else {
960 $reason = htmlspecialchars( wfMsg( 'protectreason' ) );
961 return $this->confirmProtect( '', $reason, $limit );
962 }
963 }
964
965 # Output protection confirmation dialog
966 function confirmProtect( $par, $reason, $limit = 'sysop' )
967 {
968 global $wgOut;
969
970 wfDebug( "Article::confirmProtect\n" );
971
972 $sub = htmlspecialchars( $this->mTitle->getPrefixedText() );
973 $wgOut->setRobotpolicy( 'noindex,nofollow' );
974
975 $check = '';
976 $protcom = '';
977
978 if ( $limit === '' ) {
979 $wgOut->setSubtitle( wfMsg( 'unprotectsub', $sub ) );
980 $wgOut->addWikiText( wfMsg( 'confirmunprotecttext' ) );
981 $check = htmlspecialchars( wfMsg( 'confirmunprotect' ) );
982 $protcom = htmlspecialchars( wfMsg( 'unprotectcomment' ) );
983 $formaction = $this->mTitle->escapeLocalURL( 'action=unprotect' . $par );
984 } else {
985 $wgOut->setSubtitle( wfMsg( 'protectsub', $sub ) );
986 $wgOut->addWikiText( wfMsg( 'confirmprotecttext' ) );
987 $check = htmlspecialchars( wfMsg( 'confirmprotect' ) );
988 $protcom = htmlspecialchars( wfMsg( 'protectcomment' ) );
989 $formaction = $this->mTitle->escapeLocalURL( 'action=protect' . $par );
990 }
991
992 $confirm = htmlspecialchars( wfMsg( 'confirm' ) );
993
994 $wgOut->addHTML( "
995 <form id='protectconfirm' method='post' action=\"{$formaction}\">
996 <table border='0'>
997 <tr>
998 <td align='right'>
999 <label for='wpReasonProtect'>{$protcom}:</label>
1000 </td>
1001 <td align='left'>
1002 <input type='text' size='60' name='wpReasonProtect' id='wpReasonProtect' value=\"" . htmlspecialchars( $reason ) . "\" />
1003 </td>
1004 </tr>
1005 <tr>
1006 <td>&nbsp;</td>
1007 </tr>
1008 <tr>
1009 <td align='right'>
1010 <input type='checkbox' name='wpConfirmProtect' value='1' id='wpConfirmProtect' />
1011 </td>
1012 <td>
1013 <label for='wpConfirmProtect'>{$check}</label>
1014 </td>
1015 </tr>
1016 <tr>
1017 <td>&nbsp;</td>
1018 <td>
1019 <input type='submit' name='wpConfirmProtectB' value=\"{$confirm}\" />
1020 </td>
1021 </tr>
1022 </table>
1023 </form>\n" );
1024
1025 $wgOut->returnToMain( false );
1026 }
1027
1028 function unprotect()
1029 {
1030 return $this->protect( '' );
1031 }
1032
1033 # UI entry point for page deletion
1034 function delete()
1035 {
1036 global $wgUser, $wgOut, $wgMessageCache, $wgRequest;
1037 $fname = 'Article::delete';
1038 $confirm = $wgRequest->getBool( 'wpConfirm' ) && $wgRequest->wasPosted();
1039 $reason = $wgRequest->getText( 'wpReason' );
1040
1041 # This code desperately needs to be totally rewritten
1042
1043 # Check permissions
1044 if ( ( ! $wgUser->isSysop() ) ) {
1045 $wgOut->sysopRequired();
1046 return;
1047 }
1048 if ( wfReadOnly() ) {
1049 $wgOut->readOnlyPage();
1050 return;
1051 }
1052
1053 # Better double-check that it hasn't been deleted yet!
1054 $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) );
1055 if ( ( '' == trim( $this->mTitle->getText() ) )
1056 or ( $this->mTitle->getArticleId() == 0 ) ) {
1057 $wgOut->fatalError( wfMsg( 'cannotdelete' ) );
1058 return;
1059 }
1060
1061 if ( $confirm ) {
1062 $this->doDelete( $reason );
1063 return;
1064 }
1065
1066 # determine whether this page has earlier revisions
1067 # and insert a warning if it does
1068 # we select the text because it might be useful below
1069 $ns = $this->mTitle->getNamespace();
1070 $title = $this->mTitle->getDBkey();
1071 $etitle = wfStrencode( $title );
1072 $sql = "SELECT old_text,old_flags FROM old WHERE old_namespace=$ns and old_title='$etitle' ORDER BY inverse_timestamp LIMIT 1";
1073 $res = wfQuery( $sql, DB_READ, $fname );
1074 if( ($old=wfFetchObject($res)) && !$confirm ) {
1075 $skin=$wgUser->getSkin();
1076 $wgOut->addHTML('<b>'.wfMsg('historywarning'));
1077 $wgOut->addHTML( $skin->historyLink() .'</b>');
1078 }
1079
1080 $sql="SELECT cur_text FROM cur WHERE cur_namespace=$ns and cur_title='$etitle'";
1081 $res=wfQuery($sql, DB_READ, $fname);
1082 if( ($s=wfFetchObject($res))) {
1083
1084 # if this is a mini-text, we can paste part of it into the deletion reason
1085
1086 #if this is empty, an earlier revision may contain "useful" text
1087 $blanked = false;
1088 if($s->cur_text!="") {
1089 $text=$s->cur_text;
1090 } else {
1091 if($old) {
1092 $text = Article::getRevisionText( $old );
1093 $blanked = true;
1094 }
1095
1096 }
1097
1098 $length=strlen($text);
1099
1100 # this should not happen, since it is not possible to store an empty, new
1101 # page. Let's insert a standard text in case it does, though
1102 if($length == 0 && $reason === '') {
1103 $reason = wfMsg('exblank');
1104 }
1105
1106 if($length < 500 && $reason === '') {
1107
1108 # comment field=255, let's grep the first 150 to have some user
1109 # space left
1110 $text=substr($text,0,150);
1111 # let's strip out newlines and HTML tags
1112 $text=preg_replace('/\"/',"'",$text);
1113 $text=preg_replace('/\</','&lt;',$text);
1114 $text=preg_replace('/\>/','&gt;',$text);
1115 $text=preg_replace("/[\n\r]/",'',$text);
1116 if(!$blanked) {
1117 $reason=wfMsg('excontent'). " '".$text;
1118 } else {
1119 $reason=wfMsg('exbeforeblank') . " '".$text;
1120 }
1121 if($length>150) { $reason .= '...'; } # we've only pasted part of the text
1122 $reason.="'";
1123 }
1124 }
1125
1126 return $this->confirmDelete( '', $reason );
1127 }
1128
1129 # Output deletion confirmation dialog
1130 function confirmDelete( $par, $reason )
1131 {
1132 global $wgOut;
1133
1134 wfDebug( "Article::confirmDelete\n" );
1135
1136 $sub = htmlspecialchars( $this->mTitle->getPrefixedText() );
1137 $wgOut->setSubtitle( wfMsg( 'deletesub', $sub ) );
1138 $wgOut->setRobotpolicy( 'noindex,nofollow' );
1139 $wgOut->addWikiText( wfMsg( 'confirmdeletetext' ) );
1140
1141 $formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par );
1142
1143 $confirm = htmlspecialchars( wfMsg( 'confirm' ) );
1144 $check = htmlspecialchars( wfMsg( 'confirmcheck' ) );
1145 $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) );
1146
1147 $wgOut->addHTML( "
1148 <form id='deleteconfirm' method='post' action=\"{$formaction}\">
1149 <table border='0'>
1150 <tr>
1151 <td align='right'>
1152 <label for='wpReason'>{$delcom}:</label>
1153 </td>
1154 <td align='left'>
1155 <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" />
1156 </td>
1157 </tr>
1158 <tr>
1159 <td>&nbsp;</td>
1160 </tr>
1161 <tr>
1162 <td align='right'>
1163 <input type='checkbox' name='wpConfirm' value='1' id='wpConfirm' />
1164 </td>
1165 <td>
1166 <label for='wpConfirm'>{$check}</label>
1167 </td>
1168 </tr>
1169 <tr>
1170 <td>&nbsp;</td>
1171 <td>
1172 <input type='submit' name='wpConfirmB' value=\"{$confirm}\" />
1173 </td>
1174 </tr>
1175 </table>
1176 </form>\n" );
1177
1178 $wgOut->returnToMain( false );
1179 }
1180
1181 # Perform a deletion and output success or failure messages
1182 function doDelete( $reason )
1183 {
1184 global $wgOut, $wgUser, $wgLang;
1185 $fname = 'Article::doDelete';
1186 wfDebug( "$fname\n" );
1187
1188 if ( $this->doDeleteArticle( $reason ) ) {
1189 $deleted = $this->mTitle->getPrefixedText();
1190
1191 $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
1192 $wgOut->setRobotpolicy( 'noindex,nofollow' );
1193
1194 $sk = $wgUser->getSkin();
1195 $loglink = $sk->makeKnownLink( $wgLang->getNsText(
1196 Namespace::getWikipedia() ) .
1197 ':' . wfMsg( 'dellogpage' ), wfMsg( 'deletionlog' ) );
1198
1199 $text = wfMsg( "deletedtext", $deleted, $loglink );
1200
1201 $wgOut->addHTML( '<p>' . $text . "</p>\n" );
1202 $wgOut->returnToMain( false );
1203 } else {
1204 $wgOut->fatalError( wfMsg( 'cannotdelete' ) );
1205 }
1206 }
1207
1208 # Back-end article deletion
1209 # Deletes the article with database consistency, writes logs, purges caches
1210 # Returns success
1211 function doDeleteArticle( $reason )
1212 {
1213 global $wgUser, $wgLang;
1214 global $wgUseSquid, $wgDeferredUpdateList, $wgInternalServer;
1215
1216 $fname = 'Article::doDeleteArticle';
1217 wfDebug( $fname."\n" );
1218
1219 $ns = $this->mTitle->getNamespace();
1220 $t = wfStrencode( $this->mTitle->getDBkey() );
1221 $id = $this->mTitle->getArticleID();
1222
1223 if ( '' == $t || $id == 0 ) {
1224 return false;
1225 }
1226
1227 $u = new SiteStatsUpdate( 0, 1, -$this->isCountable( $this->getContent( true ) ) );
1228 array_push( $wgDeferredUpdateList, $u );
1229
1230 $linksTo = $this->mTitle->getLinksTo();
1231
1232 # Squid purging
1233 if ( $wgUseSquid ) {
1234 $urls = array(
1235 $this->mTitle->getInternalURL(),
1236 $this->mTitle->getInternalURL( 'history' )
1237 );
1238 foreach ( $linksTo as $linkTo ) {
1239 $urls[] = $linkTo->getInternalURL();
1240 }
1241
1242 $u = new SquidUpdate( $urls );
1243 array_push( $wgDeferredUpdateList, $u );
1244
1245 }
1246
1247 # Client and file cache invalidation
1248 Title::touchArray( $linksTo );
1249
1250 # Move article and history to the "archive" table
1251 $sql = 'INSERT INTO archive (ar_namespace,ar_title,ar_text,' .
1252 'ar_comment,ar_user,ar_user_text,ar_timestamp,ar_minor_edit,' .
1253 'ar_flags) SELECT cur_namespace,cur_title,cur_text,cur_comment,' .
1254 'cur_user,cur_user_text,cur_timestamp,cur_minor_edit,0 FROM cur ' .
1255 "WHERE cur_namespace={$ns} AND cur_title='{$t}'";
1256 wfQuery( $sql, DB_WRITE, $fname );
1257
1258 $sql = 'INSERT INTO archive (ar_namespace,ar_title,ar_text,' .
1259 'ar_comment,ar_user,ar_user_text,ar_timestamp,ar_minor_edit,' .
1260 'ar_flags) SELECT old_namespace,old_title,old_text,old_comment,' .
1261 'old_user,old_user_text,old_timestamp,old_minor_edit,old_flags ' .
1262 "FROM old WHERE old_namespace={$ns} AND old_title='{$t}'";
1263 wfQuery( $sql, DB_WRITE, $fname );
1264
1265 # Now that it's safely backed up, delete it
1266
1267 $sql = "DELETE FROM cur WHERE cur_namespace={$ns} AND " .
1268 "cur_title='{$t}'";
1269 wfQuery( $sql, DB_WRITE, $fname );
1270
1271 $sql = "DELETE FROM old WHERE old_namespace={$ns} AND " .
1272 "old_title='{$t}'";
1273 wfQuery( $sql, DB_WRITE, $fname );
1274
1275 $sql = "DELETE FROM recentchanges WHERE rc_namespace={$ns} AND " .
1276 "rc_title='{$t}'";
1277 wfQuery( $sql, DB_WRITE, $fname );
1278
1279 # Finally, clean up the link tables
1280 $t = wfStrencode( $this->mTitle->getPrefixedDBkey() );
1281
1282 Article::onArticleDelete( $this->mTitle );
1283
1284 $sql = 'INSERT INTO brokenlinks (bl_from,bl_to) VALUES ';
1285 $first = true;
1286
1287 foreach ( $linksTo as $titleObj ) {
1288 if ( ! $first ) { $sql .= ','; }
1289 $first = false;
1290 # Get article ID. Efficient because it was loaded into the cache by getLinksTo().
1291 $linkID = $titleObj->getArticleID();
1292 $sql .= "({$linkID},'{$t}')";
1293 }
1294 if ( ! $first ) {
1295 wfQuery( $sql, DB_WRITE, $fname );
1296 }
1297
1298 $sql = "DELETE FROM links WHERE l_to={$id}";
1299 wfQuery( $sql, DB_WRITE, $fname );
1300
1301 $sql = "DELETE FROM links WHERE l_from={$id}";
1302 wfQuery( $sql, DB_WRITE, $fname );
1303
1304 $sql = "DELETE FROM imagelinks WHERE il_from={$id}";
1305 wfQuery( $sql, DB_WRITE, $fname );
1306
1307 $sql = "DELETE FROM brokenlinks WHERE bl_from={$id}";
1308 wfQuery( $sql, DB_WRITE, $fname );
1309
1310 $sql = "DELETE FROM categorylinks WHERE cl_from={$id}";
1311 wfQuery( $sql, DB_WRITE, $fname );
1312
1313 $log = new LogPage( wfMsg( 'dellogpage' ), wfMsg( 'dellogpagetext' ) );
1314 $art = $this->mTitle->getPrefixedText();
1315 $log->addEntry( wfMsg( 'deletedarticle', $art ), $reason );
1316
1317 # Clear the cached article id so the interface doesn't act like we exist
1318 $this->mTitle->resetArticleID( 0 );
1319 $this->mTitle->mArticleID = 0;
1320 return true;
1321 }
1322
1323 function rollback()
1324 {
1325 global $wgUser, $wgLang, $wgOut, $wgRequest;
1326
1327 if ( ! $wgUser->isSysop() ) {
1328 $wgOut->sysopRequired();
1329 return;
1330 }
1331 if ( wfReadOnly() ) {
1332 $wgOut->readOnlyPage( $this->getContent( true ) );
1333 return;
1334 }
1335
1336 # Enhanced rollback, marks edits rc_bot=1
1337 $bot = $wgRequest->getBool( 'bot' );
1338
1339 # Replace all this user's current edits with the next one down
1340 $tt = wfStrencode( $this->mTitle->getDBKey() );
1341 $n = $this->mTitle->getNamespace();
1342
1343 # Get the last editor
1344 $sql = 'SELECT cur_id,cur_user,cur_user_text,cur_comment ' .
1345 "FROM cur WHERE cur_title='{$tt}' AND cur_namespace={$n}";
1346 $res = wfQuery( $sql, DB_READ );
1347 if( ($x = wfNumRows( $res )) != 1 ) {
1348 # Something wrong
1349 $wgOut->addHTML( wfMsg( 'notanarticle' ) );
1350 return;
1351 }
1352 $s = wfFetchObject( $res );
1353 $ut = wfStrencode( $s->cur_user_text );
1354 $uid = $s->cur_user;
1355 $pid = $s->cur_id;
1356
1357 $from = str_replace( '_', ' ', $wgRequest->getVal( 'from' ) );
1358 if( $from != $s->cur_user_text ) {
1359 $wgOut->setPageTitle(wfmsg('rollbackfailed'));
1360 $wgOut->addWikiText( wfMsg( 'alreadyrolled',
1361 htmlspecialchars( $this->mTitle->getPrefixedText()),
1362 htmlspecialchars( $from ),
1363 htmlspecialchars( $s->cur_user_text ) ) );
1364 if($s->cur_comment != '') {
1365 $wgOut->addHTML(
1366 wfMsg('editcomment',
1367 htmlspecialchars( $s->cur_comment ) ) );
1368 }
1369 return;
1370 }
1371
1372 # Get the last edit not by this guy
1373 $sql = 'SELECT old_text,old_user,old_user_text,old_timestamp,old_flags ' .
1374 'FROM old USE INDEX (name_title_timestamp)' .
1375 "WHERE old_namespace={$n} AND old_title='{$tt}'" .
1376 "AND (old_user <> {$uid} OR old_user_text <> '{$ut}')" .
1377 'ORDER BY inverse_timestamp LIMIT 1';
1378 $res = wfQuery( $sql, DB_READ );
1379 if( wfNumRows( $res ) != 1 ) {
1380 # Something wrong
1381 $wgOut->setPageTitle(wfMsg('rollbackfailed'));
1382 $wgOut->addHTML( wfMsg( 'cantrollback' ) );
1383 return;
1384 }
1385 $s = wfFetchObject( $res );
1386
1387 if ( $bot ) {
1388 # Mark all reverted edits as bot
1389 $sql = 'UPDATE recentchanges SET rc_bot=1 WHERE' .
1390 "rc_cur_id=$pid AND rc_user=$uid AND rc_timestamp > '{$s->old_timestamp}'";
1391 wfQuery( $sql, DB_WRITE, $fname );
1392 }
1393
1394 # Save it!
1395 $newcomment = wfMsg( 'revertpage', $s->old_user_text, $from );
1396 $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
1397 $wgOut->setRobotpolicy( 'noindex,nofollow' );
1398 $wgOut->addHTML( '<h2>' . $newcomment . "</h2>\n<hr />\n" );
1399 $this->updateArticle( Article::getRevisionText( $s ), $newcomment, 1, $this->mTitle->userIsWatching(), $bot );
1400 Article::onArticleEdit( $this->mTitle );
1401 $wgOut->returnToMain( false );
1402 }
1403
1404
1405 # Do standard deferred updates after page view
1406
1407 /* private */ function viewUpdates()
1408 {
1409 global $wgDeferredUpdateList;
1410 if ( 0 != $this->getID() ) {
1411 global $wgDisableCounters;
1412 if( !$wgDisableCounters ) {
1413 Article::incViewCount( $this->getID() );
1414 $u = new SiteStatsUpdate( 1, 0, 0 );
1415 array_push( $wgDeferredUpdateList, $u );
1416 }
1417 $u = new UserTalkUpdate( 0, $this->mTitle->getNamespace(),
1418 $this->mTitle->getDBkey() );
1419 array_push( $wgDeferredUpdateList, $u );
1420 }
1421 }
1422
1423 # Do standard deferred updates after page edit.
1424 # Every 1000th edit, prune the recent changes table.
1425
1426 /* private */ function editUpdates( $text )
1427 {
1428 global $wgDeferredUpdateList, $wgDBname, $wgMemc;
1429 global $wgMessageCache;
1430
1431 wfSeedRandom();
1432 if ( 0 == mt_rand( 0, 999 ) ) {
1433 $cutoff = wfUnix2Timestamp( time() - ( 7 * 86400 ) );
1434 $sql = "DELETE FROM recentchanges WHERE rc_timestamp < '{$cutoff}'";
1435 wfQuery( $sql, DB_WRITE );
1436 }
1437 $id = $this->getID();
1438 $title = $this->mTitle->getPrefixedDBkey();
1439 $shortTitle = $this->mTitle->getDBkey();
1440
1441 $adj = $this->mCountAdjustment;
1442
1443 if ( 0 != $id ) {
1444 $u = new LinksUpdate( $id, $title );
1445 array_push( $wgDeferredUpdateList, $u );
1446 $u = new SiteStatsUpdate( 0, 1, $adj );
1447 array_push( $wgDeferredUpdateList, $u );
1448 $u = new SearchUpdate( $id, $title, $text );
1449 array_push( $wgDeferredUpdateList, $u );
1450
1451 $u = new UserTalkUpdate( 1, $this->mTitle->getNamespace(), $shortTitle );
1452 array_push( $wgDeferredUpdateList, $u );
1453
1454 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1455 $wgMessageCache->replace( $shortTitle, $text );
1456 }
1457 }
1458 }
1459
1460 /* private */ function setOldSubtitle()
1461 {
1462 global $wgLang, $wgOut;
1463
1464 $td = $wgLang->timeanddate( $this->mTimestamp, true );
1465 $r = wfMsg( 'revisionasof', $td );
1466 $wgOut->setSubtitle( "({$r})" );
1467 }
1468
1469 # This function is called right before saving the wikitext,
1470 # so we can do things like signatures and links-in-context.
1471
1472 function preSaveTransform( $text )
1473 {
1474 global $wgParser, $wgUser;
1475 return $wgParser->preSaveTransform( $text, $this->mTitle, $wgUser, ParserOptions::newFromUser( $wgUser ) );
1476 }
1477
1478 /* Caching functions */
1479
1480 # checkLastModified returns true if it has taken care of all
1481 # output to the client that is necessary for this request.
1482 # (that is, it has sent a cached version of the page)
1483 function tryFileCache() {
1484 static $called = false;
1485 if( $called ) {
1486 wfDebug( " tryFileCache() -- called twice!?\n" );
1487 return;
1488 }
1489 $called = true;
1490 if($this->isFileCacheable()) {
1491 $touched = $this->mTouched;
1492 if( $this->mTitle->getPrefixedDBkey() == wfMsg( 'mainpage' ) ) {
1493 # Expire the main page quicker
1494 $expire = wfUnix2Timestamp( time() - 3600 );
1495 $touched = max( $expire, $touched );
1496 }
1497 $cache = new CacheManager( $this->mTitle );
1498 if($cache->isFileCacheGood( $touched )) {
1499 global $wgOut;
1500 wfDebug( " tryFileCache() - about to load\n" );
1501 $cache->loadFromFileCache();
1502 return true;
1503 } else {
1504 wfDebug( " tryFileCache() - starting buffer\n" );
1505 ob_start( array(&$cache, 'saveToFileCache' ) );
1506 }
1507 } else {
1508 wfDebug( " tryFileCache() - not cacheable\n" );
1509 }
1510 }
1511
1512 function isFileCacheable() {
1513 global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest;
1514 extract( $wgRequest->getValues( 'action', 'oldid', 'diff', 'redirect', 'printable' ) );
1515
1516 return $wgUseFileCache
1517 and (!$wgShowIPinHeader)
1518 and ($this->getID() != 0)
1519 and ($wgUser->getId() == 0)
1520 and (!$wgUser->getNewtalk())
1521 and ($this->mTitle->getNamespace() != Namespace::getSpecial())
1522 and ($action == 'view')
1523 and (!isset($oldid))
1524 and (!isset($diff))
1525 and (!isset($redirect))
1526 and (!isset($printable))
1527 and (!$this->mRedirectedFrom);
1528 }
1529
1530 # Loads cur_touched and returns a value indicating if it should be used
1531 function checkTouched() {
1532 $id = $this->getID();
1533 $sql = 'SELECT cur_touched,cur_is_redirect FROM cur WHERE cur_id='.$id;
1534 $res = wfQuery( $sql, DB_READ, 'Article::checkTouched' );
1535 if( $s = wfFetchObject( $res ) ) {
1536 $this->mTouched = $s->cur_touched;
1537 return !$s->cur_is_redirect;
1538 } else {
1539 return false;
1540 }
1541 }
1542
1543 # Edit an article without doing all that other stuff
1544 function quickEdit( $text, $comment = '', $minor = 0 ) {
1545 global $wgUser, $wgMwRedir;
1546 $fname = 'Article::quickEdit';
1547 wfProfileIn( $fname );
1548
1549 $ns = $this->mTitle->getNamespace();
1550 $dbkey = $this->mTitle->getDBkey();
1551 $encDbKey = wfStrencode( $dbkey );
1552 $timestamp = wfTimestampNow();
1553
1554 # Save to history
1555 $sql = 'INSERT INTO old (old_namespace,old_title,old_text,old_comment,old_user,old_user_text,old_timestamp,inverse_timestamp)' .
1556 'SELECT cur_namespace,cur_title,cur_text,cur_comment,cur_user,cur_user_text,cur_timestamp,99999999999999-cur_timestamp' .
1557 "FROM cur WHERE cur_namespace=$ns AND cur_title='$encDbKey'";
1558 wfQuery( $sql, DB_WRITE );
1559
1560 # Use the affected row count to determine if the article is new
1561 $numRows = wfAffectedRows();
1562
1563 # Make an array of fields to be inserted
1564 $fields = array(
1565 'cur_text' => $text,
1566 'cur_timestamp' => $timestamp,
1567 'cur_user' => $wgUser->getID(),
1568 'cur_user_text' => $wgUser->getName(),
1569 'inverse_timestamp' => wfInvertTimestamp( $timestamp ),
1570 'cur_comment' => $comment,
1571 'cur_is_redirect' => $wgMwRedir->matchStart( $text ) ? 1 : 0,
1572 'cur_minor_edit' => intval($minor),
1573 'cur_touched' => $timestamp,
1574 );
1575
1576 if ( $numRows ) {
1577 # Update article
1578 $fields['cur_is_new'] = 0;
1579 wfUpdateArray( 'cur', $fields, array( 'cur_namespace' => $ns, 'cur_title' => $dbkey ), $fname );
1580 } else {
1581 # Insert new article
1582 $fields['cur_is_new'] = 1;
1583 $fields['cur_namespace'] = $ns;
1584 $fields['cur_title'] = $dbkey;
1585 $fields['cur_random'] = $rand = number_format( mt_rand() / mt_getrandmax(), 12, '.', '' );
1586 wfInsertArray( 'cur', $fields, $fname );
1587 }
1588 wfProfileOut( $fname );
1589 }
1590
1591 /* static */ function incViewCount( $id )
1592 {
1593 $id = intval( $id );
1594 global $wgHitcounterUpdateFreq;
1595
1596 if( $wgHitcounterUpdateFreq <= 1 ){ //
1597 wfQuery('UPDATE cur SET cur_counter = cur_counter + 1 ' .
1598 'WHERE cur_id = '.$id, DB_WRITE);
1599 return;
1600 }
1601
1602 # Not important enough to warrant an error page in case of failure
1603 $oldignore = wfIgnoreSQLErrors( true );
1604
1605 wfQuery("INSERT INTO hitcounter (hc_id) VALUES ({$id})", DB_WRITE);
1606
1607 $checkfreq = intval( $wgHitcounterUpdateFreq/25 + 1 );
1608 if( (rand() % $checkfreq != 0) or (wfLastErrno() != 0) ){
1609 # Most of the time (or on SQL errors), skip row count check
1610 wfIgnoreSQLErrors( $oldignore );
1611 return;
1612 }
1613
1614 $res = wfQuery('SELECT COUNT(*) as n FROM hitcounter', DB_WRITE);
1615 $row = wfFetchObject( $res );
1616 $rown = intval( $row->n );
1617 if( $rown >= $wgHitcounterUpdateFreq ){
1618 wfProfileIn( 'Article::incViewCount-collect' );
1619 $old_user_abort = ignore_user_abort( true );
1620
1621 wfQuery('LOCK TABLES hitcounter WRITE', DB_WRITE);
1622 wfQuery('CREATE TEMPORARY TABLE acchits TYPE=HEAP '.
1623 'SELECT hc_id,COUNT(*) AS hc_n FROM hitcounter '.
1624 'GROUP BY hc_id', DB_WRITE);
1625 wfQuery('DELETE FROM hitcounter', DB_WRITE);
1626 wfQuery('UNLOCK TABLES', DB_WRITE);
1627 wfQuery('UPDATE cur,acchits SET cur_counter=cur_counter + hc_n '.
1628 'WHERE cur_id = hc_id', DB_WRITE);
1629 wfQuery('DROP TABLE acchits', DB_WRITE);
1630
1631 ignore_user_abort( $old_user_abort );
1632 wfProfileOut( 'Article::incViewCount-collect' );
1633 }
1634 wfIgnoreSQLErrors( $oldignore );
1635 }
1636
1637 # The onArticle*() functions are supposed to be a kind of hooks
1638 # which should be called whenever any of the specified actions
1639 # are done.
1640 #
1641 # This is a good place to put code to clear caches, for instance.
1642
1643 # This is called on page move and undelete, as well as edit
1644 /* static */ function onArticleCreate($title_obj){
1645 global $wgUseSquid, $wgDeferredUpdateList;
1646
1647 $titles = $title_obj->getBrokenLinksTo();
1648
1649 # Purge squid
1650 if ( $wgUseSquid ) {
1651 $urls = $title_obj->getSquidURLs();
1652 foreach ( $titles as $linkTitle ) {
1653 $urls[] = $linkTitle->getInternalURL();
1654 }
1655 $u = new SquidUpdate( $urls );
1656 array_push( $wgDeferredUpdateList, $u );
1657 }
1658
1659 # Clear persistent link cache
1660 LinkCache::linksccClearBrokenLinksTo( $title_obj->getPrefixedDBkey() );
1661 }
1662
1663 /* static */ function onArticleDelete($title_obj){
1664 LinkCache::linksccClearLinksTo( $title_obj->getArticleID() );
1665 }
1666
1667 /* static */ function onArticleEdit($title_obj){
1668 LinkCache::linksccClearPage( $title_obj->getArticleID() );
1669 }
1670 }
1671
1672 ?>