3 * PHPwikiBot Main Class File
6 * @name PHPwikiBot main class
7 * @license http://www.gnu.org/licenses/gpl.html GPLv3+
11 * Include some definition and configuration
13 require_once dirname(__FILE__
).'/stddef.inc';
14 require_once INC
.'cfgparse.php';
15 require_once INC
.'exception.inc';
16 require_once INC
.'data.class.php';
20 * The main class for the bot
26 * Current User Config in config.yml
31 * Username of the bot user on the wiki
36 * Absolute path to API
46 * A key in $wiki array
51 * Wiki's full name or a friendly name
56 * Replicate DB's slave some times have trouble syncing, set this to 5
59 public $max_lag = 5; // fix slave db's lag problem
61 * Whether to output some unimportant messages
64 protected $out = true;
84 protected $loglevel = LOG_INFO
;
86 * fopen() handle of the log file
91 * friendly name of log level
94 protected $loglevelname = array(
96 LG_DEBUG
=> 'Debug Info',
97 LG_NOTICE
=> 'Notice',
100 LG_FATAL
=> 'Fatal Error',
103 * Whether to output the log to STDOUT/STDERR
106 protected $output_log = true;
108 * Data to pass to put_page() from create_page() or edit_page()
111 protected $editdetails;
113 /* Magic Functions */
115 * Constructor, initialize the object and login
117 * @param string $user A username in config.php
118 * @param bool $slient Be quiet
119 * @return void This function throws exception rather than return value since constructor doesn't return
120 * @throws LoginFailure from PHPwikiBot::login
123 public function __construct($user, $slient = false) {
124 /** Configuration Mapping */
125 $this->conf
= $GLOBALS['users'][$user]; // Map the user configuration array
126 $this->useragent
= $GLOBALS['useragent']; // Define the user-agent
127 $this->wikid
= $this->conf
['wiki']; // Wiki ID
128 $this->wikiname
= $GLOBALS['wiki'][$this->wikid
]['name'];// Wiki's name
129 $this->api_url
= $GLOBALS['wiki'][$this->wikid
]['api'];// Path to API
130 $this->epm
= 60 / $GLOBALS['wiki'][$this->wikid
]['epm']; // Edit per minute
131 $this->user
= $this->conf
['name']; // Username
133 * If the server supports OpenSSL, use encrypted password or else use plain text
135 if (function_exists('openssl_decrypt'))
136 $pass = openssl_decrypt($this->conf
['password'], 'AES-128-ECB', $GLOBALS['key']); // Password
138 $pass = $this->conf
['password']; // Password
139 if ($slient) $this->out
= false; // Not yet used
141 $this->loglevel
= $GLOBALS['log_level']; // Loglevel
142 $this->logh
= fopen($GLOBALS['logfile'], 'a');// Open Logfile
143 $this->output_log
= $GLOBALS['output_log']; // Whether to output logs to stdout/stderr
145 $this->conninit(); // Initialize cURL handles
148 $this->login($user, $pass);
149 } catch (LoginFailure
$e) {
155 * Clear the cookies when the script terminates or the bot object is destroyed
157 function __destruct() {
158 $this->logout(); // Logs out
159 fclose($this->logh
); // Close the log handle
163 * Convert the object to string
165 * @return string Basic info about this object
168 function __toString() {
169 // Get all required info
170 $name = $this->user
; // User name
171 $wikid = $this->wikid
; // Wiki ID in configuration
172 $wikiname = $this->wikiname
; // Wiki Name
173 $useragent = $this->useragent
; // Bot User-Agent
174 $api = $this->api_url
; // Path to API
175 if (function_exists('openssl_decrypt')) $crypt = 'yes'; // Encrypted password
179 Encrypted Password: $crypt
182 User Agent: $useragent
189 * Unexisted Method are sometimes called, in this case, a 10 Unexist Method is thrown
191 * @param string $name The name of the function
192 * @param array $arguments Array of arguments
193 * @return void No return value
194 * @throws BotException
196 public function __call($name, $arguments) {
197 // Logs before throwing exceptions
198 $this->log('Called unexist method "'.$name.'('.implode(', ', $arguments).'"!', LG_FATAL
);
199 throw new BotException('Unexist Method', 10); // Called to Unexisted Method
202 /* Public Callable Methods */
204 * Get General wiki info
206 * @param string $type The name of the setting, leave blank for all
207 * @return mixed Either the value of $type or an array contain all info
208 * @tutorial ./tutorial/gnlwikinfo.txt
211 public function wiki_info ($type = '') {
212 $response = $this->getAPI('action=query&meta=siteinfo'); // Query the server
213 if (!is_array($response)) throw new InfoFailure ('Can\'t Get Info', 300); // Failure
215 if (isset($response['query']['general'][$type]))
216 return $response['query']['general'][$type]; // Returns the information in $type
218 throw new InfoFailure ('Not in Gereral Info', 301); // Doesn't have it
220 return $response['query']['general']; // Return all
225 * Fetch a page from the wiki
227 * @param string $page The page name
228 * @return object A instance of WikiPage containing all information this function can get
229 * @throws GetPageFailure when failure
232 public function get_page($page, $internal = false) {
233 $response = $this->getAPI('action=query&prop=revisions&titles='.urlencode($page).'&rvprop=content'); // Query
234 //var_dump($response);
235 if (is_array($response)) { // Make sure there are no errors
236 $array = $response['query']['pages']; // Create a temperory variable
238 foreach ($array as $v) {
239 if (isset($v['missing'])):
240 if (!$internal) // Only logs in out-class use
241 $this->log('Page \''.$page.'\' doesn\'t exist!', LG_ERROR
);
242 throw new GetPageFailure('Page doesn\'t exist', 201);
243 elseif (isset($v['invalid'])):
244 if (!$internal) // Only logs in out-class use
245 $this->log('Page title \''.$page.'\' is invalid!', LG_ERROR
);
246 throw new GetPageFailure('Page title invaild', 202);
247 elseif (isset($v['special'])):
248 if (!$internal) // Only logs in out-class use
249 $this->log('Page \''.$page.'\' is a special page!', LG_ERROR
);
250 throw new GetPageFailure('Special Page', 203);
252 if (isset($v['revisions'][0]['*']) && is_string($v['revisions'][0]['*'])): // If every thing is proper
253 $i = new WikiPage
; // Create a new WikiPage class
254 $i->text
= $v['revisions'][0]['*']; // Page text
255 $i->title
= $v['title']; // Title
256 $i->ns
= $v['ns']; // Namespace ID
257 $i->id
= $v['pageid']; // Page ID
258 $j = strstr($i->title
, ':', true); // Namespace Name
260 $i->nsname
= $j; // A page with named namespace
262 $i->nsname
= true; // The main namespace
263 return $i; // Returns the class
265 $this->log('Can\' fetch page \''.$page.'\' for some reason!', LG_ERROR
);
266 throw new GetPageFailure('Can\'t Fetch Page', 200);
271 $this->log('Can\' fetch page \''.$page.'\' for some reason!', LG_ERROR
);
272 throw new GetPageFailure('Can\'t Fetch Page', 200);
277 * Get a page's category
279 * @param string $page The page name
280 * @return array An array with all categories or false if no category
283 public function get_page_cat($page) {
284 $response = $this->postAPI('action=query&prop=categories&titles='.urlencode($page)); // Query
285 //var_dump($response);
286 foreach ($response['query']['pages'] as $key => $value) { // Foreach into the array
288 if (!isset($value['categories'])) return false; // No categories
289 foreach ($value['categories'] as $key2 => $value2)
290 $cats[] = $value2['title']; // Get all categories into the array
298 * Get all page in a category
300 * @param string $category Name of Category
301 * @param int $limit Number of page to fetch before it stops
302 * @param string $start Start from page name
303 * @param string $ns Namespace, 'all' for all
304 * @return array Array of page
307 public function category($category, $limit = 500, $start = '', $ns = 'all') {
308 $query = 'action=query&list=categorymembers&cmtitle=' . urlencode('Category:' . $category) . '&cmlimit=' . $limit; // Query template
310 $query .= '&cmnamespace=' . $ns; // Namespace Selection
312 $query .= '&cmcontinue=' . urlencode($start); // Begins at page..., Continuing
313 $result = $this->postAPI($query); // Query
314 $cm = $result['query']['categorymembers']; // Temp variable
315 $pages = array(); // Array of pages
316 $j = count($cm); // Amount of pages
317 for ($i = 0; $i < $j; ++
$i)
318 $pages[] = $cm[$i]['title']; // Get the page name into $pages
319 if (isset($result['query-continue']['categorymembers']['cmcontinue'])) { // Continuing
320 $next = $result['query-continue']['categorymembers']['cmcontinue'];
322 array_push($pages, $next); // Push the array
324 return $pages; // Returns $pages
330 * @param string $page Page title
331 * @param string $text New Text
332 * @param string $summary Edit Summary
333 * @param bool $minor Minor Edit
334 * @param bool $force Force Edit
335 * @return bool Return true on success
336 * @throws EditFailure
338 public function create_page($page, $text, $summary, $minor = false, $force = false) {
339 $response = $this->postAPI('action=query&prop=info|revisions&intoken=edit&titles=' . urlencode($page)); // Get a token and basic info
340 $this->editdetails
= $response['query']['pages']; // Assign info to be used with put_page
341 if (!isset($this->editdetails
[-1])) throw new EditFailure('Page Exists', 420); // Page existed
342 $bot = false; // Not using bot account
343 if (isset($this->conf
['bot']) && $this->conf
['bot'] == true) $bot = true; // Using bot account
344 try { // Try to put_page()
345 $this->put_page($page, $text, $summary, $minor, $bot);
347 } catch (EditFailure
$e) {
348 throw $e; // Rethrow the exception
350 $this->editdetails
= null; // Clears the shared property
356 * @param string $page Page title
357 * @param string $text New Text
358 * @param string $summary Edit Summary
359 * @param bool $minor Minor Edit
360 * @param bool $force Force Edit
361 * @return bool Return true on success
362 * @throws EditFailure
364 public function edit_page($page, $text, $summary, $minor = false, $force = false) {
365 $response = $this->postAPI('action=query&prop=info|revisions&intoken=edit&titles=' . urlencode($page)); // Get data
366 $this->editdetails
= $response['query']['pages']; // Push the data into shared property
367 if (isset($this->editdetails
[-1])) throw new EditFailure('Page Doesn\'t Exist', 421); // Page doesn't exist
368 $bot = false; // Not using bot account
369 if (isset($this->conf
['bot']) && $this->conf
['bot'] == true) $bot = true; // Using bot account
370 try { // Try to put_page()
371 $this->put_page($page, $text, $summary, $minor, $bot);
373 } catch (EditFailure
$e) {
374 throw $e; // Rethrow the exception
376 $this->editdetails
= null; // Clears the shared property
382 * @param string $from The source page
383 * @param string $to The destination
384 * @param string $reason Reason for moving
385 * @param bool $talk Move talk page
386 * @param bool $sub Move subpages
387 * @param bool $redirect Create a redirect form $from to $to
388 * @return bool Return ture on sucess
389 * @throws MoveFailure
391 public function move_page($from, $to, $reason = '', $talk = true, $sub = true, $redirect = true) {
392 $response = $this->postAPI('action=query&prop=info&intoken=move&titles=' . urlencode($from));
393 //var_dump($response);
394 foreach ($response['query']['pages'] as $v) {
395 if (isset($v['invalid'])) throw new ProtectFailure('Invalid Title', 507);
396 $token = $v['movetoken'];
398 $query = 'action=move&from='.urlencode($from).'&to='.urlencode($to).'&token='.urlencode($token).'&reason='.urlencode($reason);
400 $query .= '&noredirect';
402 $query .= '&movetalk';
404 $query .= '&movesubpages';
405 $response = $this->postAPI($query);
406 //var_dump($response);
407 if (isset($response['error'])) {
408 switch ($response['error']['code']):
409 case 'articleexists': // 501 Destination Exists
410 throw new MoveFailure('Destination Exists', 501);
412 case 'protectedpage':
413 case 'protectedtitle':
414 case 'immobilenamespace': // 502 Protected
415 throw new MoveFailure('Protected', 502);
419 case 'cantmove-anon': // 503 Forbidden
420 throw new MoveFailure('Forbidden', 503);
422 case 'filetypemismatch': // 504 Extension Mismatch
423 throw new MoveFailure('Extension Mismatch', 504);
425 case 'nonfilenamespace': // 504 Wrong Namespace
426 throw new MoveFailure('Wrong Namespace', 505);
428 case 'selfmove': // 506 Self Move
429 throw new MoveFailure('Self Move', 506);
432 throw new MoveFailure('Move Failure', 500);
441 * @param string $page Page to delete
442 * @param string $reason Reason of deleting
443 * @return bool True when success
444 * @throws DeleteFailure
447 public function del_page($page, $reason = '') {
448 $response = $this->postAPI('action=query&prop=info&intoken=delete&titles=' . urlencode($page));
449 //var_dump($response);
450 if (isset($response['warnings']['info']['*']) && strstr($response['warnings']['info']['*'], 'not allowed'))
451 throw new DeleteFailure('Forbidden', 603);
452 foreach ($response['query']['pages'] as $v) {
453 if (isset($v['invalid'])) throw new ProtectFailure('Invalid Title', 604);
454 $token = $v['deletetoken'];
456 $query = 'action=delete&title='.urlencode($page).'&token='.urlencode($token).'&reason='.urlencode($reason);
457 $response = $this->postAPI($query);
458 if (isset($response['error'])) {
459 switch ($response['error']['code']):
462 $this->log('Failed to delete '.$page.' with error 601 No Such Page', LG_ERROR
);
463 throw new DeleteFailure('No Such Page', 601);
466 case 'autoblocked': // 402 Blocked
467 $this->log('Failed to delete '.$page.' with error 602 Blocked', LG_ERROR
);
468 throw new DeleteFailure('Blocked', 602);
470 case 'permissiondenied':
471 case 'protectedtitle':
472 case 'protectedpage':
473 case 'protectednamespace': // 603 Forbidden
474 $this->log('Failed to delete '.$page.' with error 603 Forbidden', LG_ERROR
);
475 throw new DeleteFailure('Forbidden', 603);
478 $this->log('Failed to delete '.$page.' with error 600 Delete Failure', LG_ERROR
);
479 throw new DeleteFailure('Delete Failure', 600);
486 * Undeletes a page with all revisions
488 * @param string $page Page name to undelete
489 * @param string $reason Reason of undeleting
490 * @return bool Return true on success
491 * @throws UndeleteFailure
493 public function undel_page($page, $reason = '') {
494 $response = $this->postAPI('action=query&prop=info&intoken=edit&titles=xxxxxxxx');
495 //var_dump($response);
496 foreach ($response['query']['pages'] as $v)
497 $token = $v['edittoken'];
499 $query = 'action=undelete&title='.urlencode($page).'&token='.urlencode($token).'&reason='.urlencode($reason);
500 $response = $this->postAPI($query);
501 //var_dump($response);
502 if (isset($response['error'])) {
503 switch ($response['error']['code']) {
505 $this->log('Failed to undelete '.$page.' with error 901 Not Deleted', LG_ERROR
);
506 throw new UndeleteFailure('No Such Page', 901);
509 case 'autoblocked': // 402 Blocked
510 $this->log('Failed to undelete '.$page.' with error 902 Blocked', LG_ERROR
);
511 throw new UndeleteFailure('Blocked', 902);
513 case 'permissiondenied':
514 case 'protectedtitle':
515 case 'protectedpage':
516 case 'protectednamespace': // 603 Forbidden
517 $this->log('Failed to undelete '.$page.' with error 903 Forbidden', LG_ERROR
);
518 throw new UndeleteFailure('Forbidden', 903);
521 $this->log('Failed to undelete '.$page.' with error 904 Invaild Title', LG_ERROR
);
522 throw new UndeleteFailure('Invaild Title', 904);
525 $this->log('Failed to undelete '.$page.' with error 900 Delete Failure', LG_ERROR
);
526 throw new UneleteFailure('Delete Failure', 900);
535 * @param string $name Username
536 * @param string $reason Reason for blocking
537 * @param string $exp A realtive(e.g. 2 days) or absolute(yyyymmddhhmmss)
538 * @param bool $nocreate Block the IP from creating acounts
539 * @param bool $auto Block the user's registration IP and any other IP the user tries to logon
540 * @param bool $noemail Blocks the user's ability to send emails
541 * @return bool True on success
542 * @throws BlockFailure
544 public function block ($name, $reason = '', $exp = 'never', $nocreate = false, $auto = false, $noemail = true) {
545 $resp = $this->postAPI('action=query&prop=info&intoken=block&titles=User:'.$name);
547 if (isset($resp['warnings']['info']['*']) && strstr($resp['warnings']['info']['*'], 'not allowed')) {
548 $this->log('Failed to block user '.$name.' with error 1003 Forbidden', LG_ERROR
);
549 throw new BlockFailure('Forbidden', 1003);
551 foreach ($resp['query']['pages'] as $v)
552 $token = $v['blocktoken'];
554 $query = 'action=block&user='.urlencode($name).'&expiry='.urlencode($exp).'&token='.urlencode($token);
556 $query .= '&reason='.$reason;
558 $query .= '&reason='.urlencode('I Hate '.$name);
560 $query .= '&autoblock';
562 $query .= '&nocreate';
564 $query .= '&noemail';
565 $resp = $this->postAPI($query);
567 if (isset($response['error'])) {
568 switch ($response['error']['code']) {
569 case 'alreadyblocked':
570 $this->log('Failed to block user '.$name.' with error 1001 Already Blocked', LG_ERROR
);
571 throw new BlockFailure('1001 Already Blocked', 1001);
575 $this->log('Failed to block user '.$name.' with error 1002 Blocked', LG_ERROR
);
576 throw new BlockFailure('Blocked', 1002);
578 case 'permissiondenied':
580 case 'cantblock-email':
581 case 'rangedisabled':
582 $this->log('Failed to block user '.$name.' with error 1003 Forbidden', LG_ERROR
);
583 throw new BlockFailure('Forbidden', 1003);
585 case 'invalidexpiry':
588 $this->log('Failed to block user '.$name.' with error 1004 Invaild Expiry', LG_ERROR
);
589 throw new BlockFailure('Invaild Expiry', 1004);
593 $this->log('Failed to block user '.$name.' with error 1005 Invaild User/IP', LG_ERROR
);
594 throw new BlockFailure('Invaild User/IP', 1005);
597 $this->log('Failed to block user '.$name.' with error 1000 Block Failure', LG_ERROR
);
598 throw new BlockFailure('Block Failure', 1000);
607 * @param string $name Username
608 * @param string $reason Reason for unblocking
609 * @return bool True on success
610 * @throws BlockFailure
612 public function unblock ($name, $reason = '') {
613 $resp = $this->postAPI('action=query&prop=info&intoken=unblock&titles=User:'.$name);
615 if (isset($resp['warnings']['info']['*']) && strstr($resp['warnings']['info']['*'], 'not allowed')) {
616 $this->log('Failed to unblock user '.$name.' with error 1003 Forbidden', LG_ERROR
);
617 throw new BlockFailure('Forbidden', 1003);
619 foreach ($resp['query']['pages'] as $v)
620 $token = $v['blocktoken'];
622 $query = 'action=unblock&user='.urlencode($name).'&token='.urlencode($token);
624 $query .= '&reason='.$reason;
626 $query .= '&reason='.urlencode('Sorry '.$name);
627 $resp = $this->postAPI($query);
629 if (isset($response['error'])) {
630 switch ($response['error']['code']) {
633 $this->log('Failed to unblock user '.$name.' with error 1002 Blocked', LG_ERROR
);
634 throw new BlockFailure('Blocked', 1002);
636 case 'permissiondenied':
638 $this->log('Failed to unblock user '.$name.' with error 1003 Forbidden', LG_ERROR
);
639 throw new BlockFailure('Forbidden', 1003);
642 $this->log('Failed to unblock user '.$name.' with error 1007 Not Blocked', LG_ERROR
);
643 throw new BlockFailure('Not Blocked', 1007);
646 $this->log('Failed to unblock user '.$name.' with error 1000 Unblock Failure', LG_ERROR
);
647 throw new BlockFailure('Unblock Failure', 1000);
656 * @param string $page Page title to protect
657 * @param string $edit all=everyone autoconfirmed=Autoconfirmed Users sysop=Administrators
658 * @param string $move all=everyone autoconfirmed=Autoconfirmed Users sysop=Administrators
659 * @param string $reason Reason of protection
660 * @param string $editexp Edit protecting expiry in format yyyymmddhhmmss
661 * @param string $movexp Move protecting expiry in format yyyymmddhhmmss
662 * @param bool $cascade Whether to enable cascade protection, i.e. protect all transcluded tamplates
663 * @return bool Return true on success
664 * @throws ProtectFailure
666 public function protect_page($page, $edit, $move, $reason = '', $editexp = 'never', $movexp = 'never', $cascade = false) {
667 $response = $this->postAPI('action=query&prop=info&intoken=protect&titles=' . urlencode($page));
668 //var_dump($response);
669 if (isset($response['warnings']['info']['*']) && strstr($response['warnings']['info']['*'], 'not allowed'))
670 throw new ProtectFailure('Forbidden', 703);
671 foreach ($response['query']['pages'] as $v) {
672 if (isset($v['invalid'])) throw new ProtectFailure('Invalid Title', 704);
673 $token = $v['protecttoken'];
675 $query = 'action=protect&title='.urlencode($page).'&token='.urlencode($token);
677 $query .= '&reason='.urlencode($reason);
678 $query .= '&protections=edit='.$edit.'|move='.$move;
679 $query .= '&expiry='.$editexp.'|'.$movexp;
680 if ($cascade) $query .= '&cascade';
681 $response = $this->postAPI($query);
682 //var_dump($response);
683 if (isset($response['error'])) {
684 switch ($response['error']['code']) {
685 case 'missingtitle-createonly':
686 $this->log('Failed to protect '.$page.' with error 701 No Such Page', LG_ERROR
);
687 throw new ProtectFailure('No Such Page', 701);
690 case 'autoblocked': // 702 Blocked
691 $this->log('Failed to protect '.$page.' with error 702 Blocked', LG_ERROR
);
692 throw new ProtectFailure('Blocked', 702);
695 case 'permissiondenied':
696 case 'protectednamespace': // 703 Forbidden
697 $this->log('Failed to protect '.$page.' with error 703 Forbidden', LG_ERROR
);
698 throw new ProtectFailure('Forbidden', 703);
700 case 'invalidexpiry': // 705 Invaild Expiry
701 $this->log('Failed to protect '.$page.' with error 705 Invaild Expiry', LG_ERROR
);
702 throw new ProtectFailure('Invaild Expiry', 705);
704 case 'pastexpiry': // 706 Past Expiry
705 $this->log('Failed to protect '.$page.' with error 706 Past Expiry', LG_ERROR
);
706 throw new ProtectFailure('Past Expiry', 706);
708 case 'protect-invalidlevel': // 707 Invaild Level
709 $this->log('Failed to protect '.$page.' with error 707 Invaild Level', LG_ERROR
);
710 throw new ProtectFailure('Invaild Level', 707);
713 $this->log('Failed to protect '.$page.' with error 700 Protect Failure', LG_ERROR
);
714 throw new ProtectFailure('Protect Failure', 700);
721 * Protects a non-exist page
723 * @param string $page Page title to protect
724 * @param string $perm Permission:all=everyone autoconfirmed=Autoconfirmed Users sysop=Administrators
725 * @param string $reason Reason of protection
726 * @param string $exp Protecting expiry in format yyyymmddhhmmss
727 * @return bool Return true on success
728 * @throws ProtectFailure
730 public function protect_title($page, $perm, $reason = '', $exp = 'never') {
731 $response = $this->postAPI('action=query&prop=info&intoken=protect&titles=' . urlencode($page));
732 //var_dump($response);
733 if (isset($response['warnings']['info']['*']) && strstr($response['warnings']['info']['*'], 'not allowed'))
734 throw new ProtectFailure('Forbidden', 703);
735 foreach ($response['query']['pages'] as $v) {
736 if (isset($v['invalid'])) throw new ProtectFailure('Invalid Title', 704);
737 $token = $v['protecttoken'];
739 $query = 'action=protect&title='.urlencode($page).'&token='.urlencode($token);
741 $query .= '&reason='.urlencode($reason);
742 $query .= '&protections=create='.$perm;
743 $query .= '&expiry='.$exp;
744 $response = $this->postAPI($query);
745 //var_dump($response);
746 if (isset($response['error'])) {
747 switch ($response['error']['code']) {
748 case 'create-titleexists':
749 $this->log('Failed to protect '.$page.' with error 708 Page Exists', LG_ERROR
);
750 throw new ProtectFailure('Page Exists', 708);
753 case 'autoblocked': // 702 Blocked
754 $this->log('Failed to protect '.$page.' with error 702 Blocked', LG_ERROR
);
755 throw new ProtectFailure('Blocked', 702);
758 case 'permissiondenied':
759 case 'protectednamespace': // 703 Forbidden
760 $this->log('Failed to protect '.$page.' with error 703 Forbidden', LG_ERROR
);
761 throw new ProtectFailure('Forbidden', 703);
763 case 'invalidexpiry': // 705 Invaild Expiry
764 $this->log('Failed to protect '.$page.' with error 705 Invaild Expiry', LG_ERROR
);
765 throw new ProtectFailure('Invaild Expiry', 705);
767 case 'pastexpiry': // 706 Past Expiry
768 $this->log('Failed to protect '.$page.' with error 706 Past Expiry', LG_ERROR
);
769 throw new ProtectFailure('Past Expiry', 706);
771 case 'protect-invalidlevel': // 707 Invaild Level
772 $this->log('Failed to protect '.$page.' with error 707 Invaild Level', LG_ERROR
);
773 throw new ProtectFailure('Invaild Level', 707);
776 $this->log('Failed to protect '.$page.' with error 700 Protect Failure', LG_ERROR
);
777 throw new ProtectFailure('Protect Failure', 700);
784 * Upload a local file or a remote file using URL
786 * @param string $src Source, may be a local file or an URI with a warpper
787 * @param string $target Target file name
788 * @param string $comment Upload comment
789 * @param string $text File page content
790 * @return class a class with file data(UploadData)
791 * @throws UploadFailure
793 function upload($src, $target, $comment = '', $text = '') {
794 $response = $this->postAPI('action=query&prop=info&intoken=edit&titles=xxxxxxxx');
795 //var_dump($response);
796 foreach ($response['query']['pages'] as $v)
797 $token = $v['edittoken'];
799 if ($this->is_url($src)) {
800 $i = get_headers($src);
802 if ($i[0]{9} != 2 and $i[0]{9} != 3) throw new UploadFailure('Can\'t Fetch File', 801);
803 $query = 'action=upload&url='.urlencode($src).'&token='.urlencode($token).'&filename='.urlencode($target);
804 if ($comment) $query .= '&comment='.urlencode($comment);
805 if ($text) $query .= '&text='.urlencode($text);
806 $response = $this->postAPI($query);
807 //var_dump($response);
809 if (!is_readable($src)) throw new UploadFailure('Can\'t Read File', 802);
811 'action' => 'upload',
814 'filename' => $target,
817 //var_dump($query['file']);
818 if ($comment) $query['comment'] = $comment;
819 if ($text) $query['text'] = $text;
822 CURLOPT_RETURNTRANSFER
=> true,
823 CURLOPT_COOKIEJAR
=> 'cookie.txt',
824 CURLOPT_COOKIEFILE
=> 'cookie.txt',
825 CURLOPT_USERAGENT
=> $this->useragent
,
826 CURLOPT_HEADER
=> false,
827 CURLOPT_URL
=> $this->api_url
,
828 CURLOPT_POST
=> true,
829 CURLOPT_POSTFIELDS
=> $query,
830 CURLOPT_HTTPHEADER
=> array('Content-Type: multipart/form-data'),
832 curl_setopt_array($ch, $cfg);
833 $response = curl_exec($ch);
834 if (curl_errno($ch)) var_dump(curl_error($ch));
836 $response = unserialize($response);
838 if (isset($response['error'])) {
839 switch ($response['error']['code']) {
841 $this->log('Failed to upload '.$src.' with error 801 Can\'t Fetch File', LG_ERROR
);
842 throw new UploadFailure('Can\'t Fetch File', 801);
844 case 'permissiondenied':
845 $this->log('Failed to upload '.$src.' with error 803 Forbidden', LG_ERROR
);
846 throw new UploadFailure('Forbidden', 803);
848 case 'autoblocked': // 804 Blocked
849 $this->log('Failed to upload '.$src.' with error 804 Blocked', LG_ERROR
);
850 throw new UploadFailure('Blocked', 804);
853 $this->log('Failed to upload '.$src.' with error 800 Upload Failure', LG_ERROR
);
854 throw new UploadFailure('Upload Failure', 800);
857 if ($response['upload']['result'] == 'Success'):
858 $j = $response['upload']['imageinfo'];
859 $this->log('Uploaded '.$src.' to '.$j['descriptionurl'], LG_INFO
);
861 $i->timestamp
= $j['timestamp'];
862 $i->width
= $j['width'];
863 $i->height
= $j['height'];
865 $i->page
= $j['descriptionurl'];
866 $i->mime
= $j['mime'];
867 $i->sha1
= $j['sha1'];
870 throw new UploadFailure('Upload Failure', 800);
876 * @param string $user The User name
877 * @param string $subject Email Subject
878 * @param string $text Content of email
879 * @param bool $cc Send a copy the the sender
880 * @return bool True on success
881 * @throws EmailFailure
883 public function email ($user, $subject, $text, $cc) {
884 $response = $this->postAPI('action=query&prop=info&intoken=email&titles=User%3A' . urlencode($user));
885 //var_dump($response);
886 if (isset($response['warnings']['info']['*']) && strstr($response['warnings']['info']['*'], 'not allowed'))
887 throw new ProtectFailure('Forbidden', 703);
888 foreach ($response['query']['pages'] as $v) {
889 if (isset($v['invalid'])) throw new ProtectFailure('Invalid Title', 704);
890 $token = $v['emailtoken'];
893 $query = 'action=emailuser&target='.urlencode($user).'&subject='.urlencode($subject).'&text='.urlencode($text).'&token='.urlencode($token);
894 if ($cc) $query .= '&ccme';
895 $resp = $this->postAPI($query);
897 if (isset($resp['error'])) switch ($resp['error']['code']) {
899 case 'usermaildisabled':
900 $this->log('Failed to email '.$user.' with error 1101 Don\'t want email', LG_ERROR
);
901 throw new EmailFailure('Don\'t want email', 1101);
903 case 'permissiondenied':
904 $this->log('Failed to email '.$user.' with error 1103 Forbidden', LG_ERROR
);
905 throw new EmailFailure('Forbidden', 1103);
908 case 'blockedfrommail':
909 $this->log('Failed to email '.$user.' with error 1102 Blocked', LG_ERROR
);
910 throw new EmailFailure('Blocked', 1102);
913 $this->log('Failed to email '.$user.' with error 1100 Email Failure', LG_ERROR
);
914 throw new EmailFailure('Email Failure', 1100);
916 if ($resp['emailuser']['result'] == 'Success') return true;
917 $this->log('Failed to email '.$user.' with error 1100 Email Failure', LG_ERROR
);
918 throw new EmailFailure('Email Failure', 1100);
921 public function ns_get() {}
924 * Imports a page from InterWiki
926 * @param string $iw InterWiki name
927 * @param string $src Source page
928 * @param int $ns Target namespace id
929 * @param bool $full Full revision import
930 * @return int Number of revisions imported
931 * @throws ImportFailure
933 public function iwimp($iw, $src, $ns = '', $full = false) {
934 $query = 'action=import&interwikisource='.urlencode($iw).'&interwikipage='.urlencode($src).'&token='.$this->imp_token();
935 if ($ns !== '') $query .= '&namespace='.$ns;
936 if ($full) $query .= '&fullhistory';
937 $resp = $this->postAPI($query);
938 if (isset($resp['error'])) switch ($resp['error']['code']) {
939 case 'unknown_interwikisource':
941 $this->log('Failed to import page '.$iw.':'.$src.' with error 1201 Wrong Interwiki, Interwiki importing for that InterWiki might be disabled', LG_ERROR
);
942 throw new ImportFailure('Wrong Interwiki', 1201);
945 $this->log('Failed to import page '.$iw.':'.$src.' with error 1203 Forbidden', LG_ERROR
);
946 throw new ImportFailure('Forbidden', 1203);
949 $this->log('Failed to import page '.$iw.':'.$src.' with error 1200 Import Failure', LG_ERROR
);
950 throw new ImportFailure('Import Failure', 1200);
952 if (isset($resp['import'][0]['revisions']))
953 return $resp['import'][0]['revisions'];
954 $this->log('Failed to import page '.$iw.':'.$src.' with error 1200 Import Failure', LG_ERROR
);
955 throw new ImportFailure('Import Failure', 1200);
959 * Imports an XML file
961 * @param string $file File name, checked in function
962 * @return int Number of revisions imported
963 * @throws ImportFailure
965 public function impxml($file) {
966 if (!is_readable($file)) {
967 $this->log('Failed to import file '.$file.' with error 1204 Bad File', LG_ERROR
);
968 throw new ImportFailure('Bad File', 1204);
970 if (function_exists('simplexml_load_file')) if (!@simplexml_load_file
($file)) {
971 $this->log('Failed to import file '.$file.' with error 1204 Bad File', LG_ERROR
);
972 throw new ImportFailure('Bad File', 1204);
975 'action' => 'import',
977 'token' => urldecode($this->imp_token()),
982 CURLOPT_RETURNTRANSFER
=> true,
983 CURLOPT_COOKIEJAR
=> 'cookie.txt',
984 CURLOPT_COOKIEFILE
=> 'cookie.txt',
985 CURLOPT_USERAGENT
=> $this->useragent
,
986 CURLOPT_HEADER
=> false,
987 CURLOPT_URL
=> $this->api_url
,
988 CURLOPT_POST
=> true,
989 CURLOPT_POSTFIELDS
=> $query,
990 CURLOPT_HTTPHEADER
=> array('Content-Type: multipart/form-data'),
992 curl_setopt_array($ch, $cfg);
993 $resp = curl_exec($ch);
994 if (curl_errno($ch)) var_dump(curl_error($ch));
996 $resp = unserialize($resp);
998 if (isset($resp['error'])) switch ($resp['error']['code']) {
999 case 'cantimport-upload':
1000 $this->log('Failed to import file '.$file.' with error 1203 Forbidden', LG_ERROR
);
1001 throw new ImportFailure('Forbidden', 1203);
1003 case 'partialupload':
1005 $this->log('Failed to import file '.$file.' with error 1202 Upload Failure', LG_ERROR
);
1006 throw new ImportFailure('Upload Failure', 1202);
1009 case 'cantopenfile':
1010 $this->log('Failed to import file '.$file.' with error 1205 Server Fault', LG_ERROR
);
1011 throw new ImportFailure('Server Fault', 1202);
1014 $this->log('Failed to import file '.$file.' with error 1200 Import Failure', LG_ERROR
);
1015 throw new ImportFailure('Import Failure', 1200);
1017 if (isset($resp['import'][0]['revisions']))
1018 return $resp['import'][0]['revisions'];
1019 $this->log('Failed to import file '.$file.' with error 1200 Import Failure', LG_ERROR
);
1020 throw new ImportFailure('Import Failure', 1200);
1024 * Purge the cache of a page
1026 * @param mixed $page An array of page names or a string of page name
1027 * @return bool True on success and false on failure
1030 public function purge($page) {
1031 $query = 'action=purge&titles=';
1032 if (is_array($page)) {
1033 foreach ($page as &$i) $i = urlencode($i);
1034 $query .= implode('|', $page);
1035 } else $query .= urlencode($page);
1036 $resp = $this->getAPI($query);
1038 if (isset($resp['error'])) {
1039 if (is_array($page))
1040 $this->log('Failed to purge pages '.implode(', ', $page).' with error 11 Purge Failure', LG_NOTICE
);
1042 $this->log('Failed to purge page '.$page.' with error 11 Purge Failure', LG_NOTICE
);
1049 * Exports a page to xml
1051 * @param mixed $page Array of titles in strings or a string of title
1052 * @return mixed An array of ExportedPage or a object of ExportedPage
1053 * @throws BotException
1055 public function export($page) {
1056 if (is_string($page)) {
1057 $resp = $this->getAPI('action=query&titles='.urlencode($page).'&export');
1059 $i = new ExportedPage
;
1060 foreach ($resp['query']['pages'] as $b) {
1061 $i->title
= $b['title'];
1062 if (isset($b['missing'])) return $i;
1063 $i->id
= $b['pageid'];
1065 $i->title
= $b['title'];
1067 $i->xml
= $resp['query']['export']['*'];
1069 } elseif (is_array($page)) {
1071 foreach ($page as $pg)
1072 $i[$pg] = $this->export($pg);
1075 $this->log('PHPwikiBot::export() requires an string or an array for argument no. 1!', LG_FATAL
);
1076 throw new BotException('Usage Error', 12);
1080 * Exports page(s) to file(s)
1082 * @param array $page Array Page name => file
1083 * @return int Number of pages exported
1086 public function export_file($page) {
1088 foreach ($page as $p => $f) {
1089 $i = $this->export($p);
1090 if ($i->xml
!== NULL) {
1091 if (file_put_contents($f, $i->xml
) === false)
1092 $this->log('Failed to export '.$p.' to '.$f);
1094 } else $this->log('Page '.$p.' doesn\'t exist!!');
1101 /* Internal Methods */
1103 * Change a page's content
1105 * @param string $name Page Name
1106 * @param string $newtext Page Content
1107 * @param string $summary Edit Summary
1108 * @param bool $minor Minor Edit
1109 * @param bool $bot Bot Edit
1110 * @param string $force Force Edit
1111 * @return bool Return true on success
1112 * @throws EditFailure
1115 protected function put_page($name, $newtext, $summary, $minor = false, $bot = true, $force = false) {
1116 foreach ($this->editdetails
as $key => $value) {
1117 $token = urlencode($value['edittoken']);
1118 $sts = $value['starttimestamp'];
1119 if (isset($this->editdetails
[-1])) {
1121 $extra = '&createonly=yes';
1123 $ts = $value['revisions'][0]['timestamp'];
1124 $extra = '&nocreate=yes';
1127 $newtext = urlencode($newtext);
1129 $rawoldtext = $this->get_page($name, true);
1130 } catch (GetPageFailure
$e) {
1131 if ($e->getCode() == 201)
1136 $oldtext = urlencode($rawoldtext);
1137 $summary = urlencode($summary);
1138 //$md5 = md5($newtext);
1140 if ($newtext == $oldtext) {
1141 //the new content is the same, nothing changes
1142 $this->log('401 Same Content, can\'t update!!!', LG_ERROR
);
1143 throw new EditFailure('Same Content', 401);
1145 if ($newtext == '' && !$force) {
1146 //the new content is void, nothing changes
1147 $this->log('402 Blank Content, use $force!!!', LG_ERROR
);
1148 throw new EditFailure('Blank Content', 402);
1150 $post = "title=$name&action=edit&basetimestamp=$ts&starttimestamp=$sts&token=$token&summary=$summary$extra&text=$newtext";
1152 if (!$this->allowBots($rawoldtext)) throw new EditFailure('Forbidden', 403);
1153 $post .= '&bot=yes';
1156 $post .= '&minor=yes';
1158 $post .= '¬minor=yes';
1160 $response = $this->postAPI($post);
1161 if (isset($response['edit']['result']) && $response['edit']['result'] == 'Success') {
1162 $this->log('Successfully edited page ' . $response['edit']['title'], LG_INFO
);
1165 /*Being worked on to throw the right exception*/
1166 } elseif (isset($response['error'])) {
1167 $this->log('[' . $response['error']['code'] . '] ' . $response['error']['info'], LG_ERROR
);
1168 switch ($response['error']['code']):
1170 case 'permissiondenied':
1171 case 'noedit': // 403 Forbidden
1172 throw new EditFailure('Forbidden', 403);
1175 case 'autoblocked': // 404 Blocked
1176 throw new EditFailure('Blocked', 404);
1178 case 'protectedtitle':
1179 case 'protectedpage':
1180 case 'protectednamespace':
1181 throw new EditFailure('Protected', 405);
1184 throw new EditFailure('MD5 Failed', 406);
1187 throw new EditFailure('Edit Failure', 400);
1190 $this->log('[' . $response['edit']['result'] . '] ' . $response['error']['info'], LG_ERROR
);
1191 throw EditFailure('Edit Failure', 400);
1196 * Fetch the import token
1198 * @return string urlencode()ed version of token
1201 protected function imp_token() {
1202 $resp = $this->postAPI('action=query&prop=info&intoken=import&titles=Main%20Page');
1204 if (isset($resp['warnings']['info']['*']) && strstr($resp['warnings']['info']['*'], 'not allowed'))
1205 throw new ImportFailure('Forbidden', 1203);
1206 foreach ($resp['query']['pages'] as $v)
1207 return urlencode($v['importtoken']);
1211 * The login method, used to logon to MediaWiki's API
1213 * @param string $user The username
1214 * @param string $pass The password
1215 * @return bool true when success
1216 * @throws LoginFailure when can't login
1219 protected function login($user, $pass) {
1220 $response = $this->postAPI('action=login&lgname=' . urlencode($user) . '&lgpassword=' . urlencode($pass));
1221 //var_dump($response);
1222 if ($response['login']['result'] == 'Success'):
1223 echo 'Logged in!'.EOL
; //Unpatched server, all done. (See bug #23076, April 2010.)
1224 elseif ($response['login']['result'] == 'NeedToken'):
1225 //Patched server, going fine
1226 $token = $response['login']['token'];
1227 $newresponse = $this->postAPI('action=login&lgname=' . urlencode($user) . '&lgpassword=' . urlencode($pass) . '&lgtoken=' . $token);
1228 //var_dump($newresponse);
1229 if ($newresponse['login']['result'] == 'Success') :
1230 echo 'Logged in!'.EOL
; //All done
1232 echo 'Forced by server to wait. Automatically trying again.', EOL
;
1234 $this->login($user, $pass);
1238 if (isset($response['login']['wait']) ||
(isset($response['error']['code']) && $response['error']['code'] == "maxlag")) {
1239 echo 'Forced by server to wait. Automatically trying again.', EOL
;
1241 $this->login($user, $pass);
1243 // die('Login failed: ' . $response . EOL);
1244 echo 'Debugging Info:', EOL
;
1245 var_dump($response);
1246 throw new LoginFailure('Can\'t Login', 100);
1252 * Logout method, clear all cookies
1254 * @return void There is no such error as can't clear cookies so this was skipped
1257 protected function logout() {
1258 $this->postAPI('action=logout');
1262 * Perform a GET request to the API
1264 * @param string $query The query string to pass the the API, without ?
1265 * @return mixed The unserialized data from the API
1268 protected function getAPI($query) {
1269 //var_dump($this->api_url.'?'.$query.'&maxlag='.$this->max_lag.'&format=php');
1270 curl_setopt($this->get
, CURLOPT_URL
, $this->api_url
.'?'.$query.'&maxlag='.$this->max_lag
.'&format=php');
1271 $response = curl_exec($this->get
);
1272 if (curl_errno($this->get
)) return curl_error($this->get
);
1273 /*$fh = fopen('test.txt', 'a');
1274 fwrite($fh, $response);
1276 //var_dump($response);
1277 return unserialize($response);
1281 * Perform a POST request to the API
1283 * @param string $postdata The data to post in this format a=b&b=c
1284 * @return mixed The unserialized data from the API
1287 protected function postAPI($postdata = '') {
1288 if ($postdata !== '') $postdata .= '&';
1289 $postdata .= 'format=php';
1290 //echo $postdata, EOL;
1291 curl_setopt($this->post
, CURLOPT_POSTFIELDS
, $postdata);
1292 $response = curl_exec($this->post
);
1293 if (curl_errno($this->post
)) return curl_error($this->post
);
1294 //echo $response, EOL;
1295 //var_dump($response);
1296 return unserialize($response);
1300 * Initiailze the class property, the $get and $post handle
1302 * @return void No return value
1305 protected function conninit() {
1306 $this->get
= curl_init();
1307 $this->post
= curl_init();
1309 'Connection: keep-alive',
1313 CURLOPT_RETURNTRANSFER
=> true,
1314 CURLOPT_COOKIEJAR
=> 'cookie.txt',
1315 CURLOPT_COOKIEFILE
=> 'cookie.txt',
1316 CURLOPT_USERAGENT
=> $this->useragent
,
1317 CURLOPT_HEADER
=> false,
1318 CURLOPT_ENCODING
=> 'gzip,deflate',
1319 CURLOPT_HTTPHEADER
=> $header,
1321 $header[] = 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8';
1323 CURLOPT_URL
=> $this->api_url
,
1324 CURLOPT_POST
=> true,
1325 CURLOPT_HTTPHEADER
=> $header,
1327 curl_setopt_array($this->get
, $cfg);
1328 curl_setopt_array($this->post
, $cfg);
1329 curl_setopt_array($this->post
, $post);
1333 * Log to file and stdout(for html)/stderr(cli)
1335 * @param string $msg Error to log
1336 * @param int $level On of the error constants
1337 * @return void No return value
1340 protected function log($msg, $level = LG_INFO
) {
1341 //var_dump($msg, $level, $this->loglevel);
1342 if ($level >= $this->loglevel
) {
1343 $msg = date('Y-m-d H:i:s').' - '.$this->loglevelname
[$level].': '.$msg;
1344 if ( $this->output_log
) {
1345 if(CLI
&& LOG_TO_STDERR
) {
1346 if ($level < LG_WARN
&& !WIN32
)
1347 fwrite(STDERR
, "\033[31m$msg\033[0m".EOL
);
1349 fwrite(STDERR
, $msg.EOL
);
1351 if ($level < LOG_WARN
)
1352 echo "\033[31m$msg\033[0m".EOL
;
1357 fwrite($this->logh
, $msg.PHP_EOL
);
1363 * See if bots are allowed to edit the page
1365 * @param string $text The content of the page
1366 * @return bool Returns true if the bot is allowed
1369 protected function allowBots($text) {
1370 if (preg_match('/\{\{(nobots|bots\|allow=none|bots\|deny=all|bots\|optout=all|bots\|deny=.*?' . preg_quote($this->user
, '/') . '.*?)\}\}/iS', $text))
1376 * Check if $url is a valid URL, doesn't check if it returns an error when requesting
1378 * @param string $url URL to check
1379 * @return bool True if $url is really a URL or false when it's some what not using HTTP(S) or FTP
1382 protected function is_url($url) {
1383 return (bool)preg_match('/^(http|https|ftp):\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"])*$/', $url);