Modifying ConfirmEdit extension to make it API-friendly:
[toast/cookiecaptcha.git] / ConfirmEdit_body.php
1 <?php
2
3 class ConfirmEditHooks {
4         static function getInstance() {
5                 global $wgCaptcha, $wgCaptchaClass, $wgExtensionMessagesFiles;
6                 static $done = false;
7                 if ( !$done ) {
8                         $done = true;
9                         wfLoadExtensionMessages( 'ConfirmEdit' );
10                         if ( isset( $wgExtensionMessagesFiles[$wgCaptchaClass] ) ) {
11                                 wfLoadExtensionMessages( $wgCaptchaClass );
12                         }
13                         $wgCaptcha = new $wgCaptchaClass;
14                 }
15                 return $wgCaptcha;
16         }
17
18         static function confirmEdit( &$editPage, $newtext, $section ) {
19                 return self::getInstance()->confirmEdit( $editPage, $newtext, $section );
20         }
21
22         static function confirmEditMerged( &$editPage, $newtext ) {
23                 return self::getInstance()->confirmEditMerged( $editPage, $newtext );
24         }
25         
26         static function confirmEditAPI( &$editPage, $newtext, &$resultArr ) {
27                 return self::getInstance()->confirmEditAPI( $editPage, $newtext, $resultArr );
28         }
29
30         static function injectUserCreate( &$template ) {
31                 return self::getInstance()->injectUserCreate( $template );
32         }
33
34         static function confirmUserCreate( $u, &$message ) {
35                 return self::getInstance()->confirmUserCreate( $u, $message );
36         }
37
38         static function triggerUserLogin( $user, $password, $retval ) {
39                 return self::getInstance()->triggerUserLogin( $user, $password, $retval );
40         }
41
42         static function injectUserLogin( &$template ) {
43                 return self::getInstance()->injectUserLogin( $template );
44         }
45
46         static function confirmUserLogin( $u, $pass, &$retval ) {
47                 return self::getInstance()->confirmUserLogin( $u, $pass, $retval );
48         }
49 }
50
51 class CaptchaSpecialPage extends UnlistedSpecialPage {
52         function execute( $par ) {
53                 $this->setHeaders();
54                 $instance = ConfirmEditHooks::getInstance();
55                 switch( $par ) {
56                 case "image":
57                         return $instance->showImage();
58                 case "help":
59                 default:
60                         return $instance->showHelp();
61                 }
62         }
63 }
64
65
66 class SimpleCaptcha {
67         function SimpleCaptcha() {
68                 global $wgCaptchaStorageClass;
69                 $this->storage = new $wgCaptchaStorageClass;
70         }
71         
72         function getCaptcha() {
73                 $a = mt_rand(0, 100);
74                 $b = mt_rand(0, 10);
75                 $op = mt_rand(0, 1) ? '+' : '-';
76
77                 $test = "$a $op $b";
78                 $answer = ($op == '+') ? ($a + $b) : ($a - $b);
79                 return array('question' => $test, 'answer' => $answer);
80         }
81         
82         function addCaptchaAPI(&$resultArr) {
83                 $captcha = $this->getCaptcha();
84                 $index = $this->storeCaptcha( $captcha );
85                 $resultArr['captcha']['type'] = 'simple';
86                 $resultArr['captcha']['id'] = $index;
87                 $resultArr['captcha']['question'] = $captcha['question'];
88         }
89         
90         /**
91          * Insert a captcha prompt into the edit form.
92          * This sample implementation generates a simple arithmetic operation;
93          * it would be easy to defeat by machine.
94          *
95          * Override this!
96          *
97          * @return string HTML
98          */
99         function getForm() {
100                 $captcha = $this->getCaptcha();
101                 $index = $this->storeCaptcha( $captcha );
102
103                 return "<p><label for=\"wpCaptchaWord\">{$captcha['question']}</label> = " .
104                         wfElement( 'input', array(
105                                 'name' => 'wpCaptchaWord',
106                                 'id'   => 'wpCaptchaWord',
107                                 'tabindex' => 1 ) ) . // tab in before the edit textarea
108                         "</p>\n" .
109                         wfElement( 'input', array(
110                                 'type'  => 'hidden',
111                                 'name'  => 'wpCaptchaId',
112                                 'id'    => 'wpCaptchaId',
113                                 'value' => $index ) );
114         }
115
116         /**
117          * Insert the captcha prompt into an edit form.
118          * @param OutputPage $out
119          */
120         function editCallback( &$out ) {
121                 $out->addWikiText( $this->getMessage( $this->action ) );
122                 $out->addHTML( $this->getForm() );
123         }
124
125         /**
126          * Show a message asking the user to enter a captcha on edit
127          * The result will be treated as wiki text
128          *
129          * @param $action Action being performed
130          * @return string
131          */
132         function getMessage( $action ) {
133                 $name = 'captcha-' . $action;
134                 $text = wfMsg( $name );
135                 # Obtain a more tailored message, if possible, otherwise, fall back to
136                 # the default for edits
137                 return wfEmptyMsg( $name, $text ) ? wfMsg( 'captcha-edit' ) : $text;
138         }
139
140         /**
141          * Inject whazawhoo
142          * @fixme if multiple thingies insert a header, could break
143          * @param SimpleTemplate $template
144          * @return bool true to keep running callbacks
145          */
146         function injectUserCreate( &$template ) {
147                 global $wgCaptchaTriggers, $wgOut;
148                 if( $wgCaptchaTriggers['createaccount'] ) {
149                         $template->set( 'header',
150                                 "<div class='captcha'>" .
151                                 $wgOut->parse( $this->getMessage( 'createaccount' ) ) .
152                                 $this->getForm() .
153                                 "</div>\n" );
154                 }
155                 return true;
156         }
157
158         /**
159          * Inject a captcha into the user login form after a failed
160          * password attempt as a speedbump for mass attacks.
161          * @fixme if multiple thingies insert a header, could break
162          * @param SimpleTemplate $template
163          * @return bool true to keep running callbacks
164          */
165         function injectUserLogin( &$template ) {
166                 if( $this->isBadLoginTriggered() ) {
167                         global $wgOut;
168                         $template->set( 'header',
169                                 "<div class='captcha'>" .
170                                 $wgOut->parse( $this->getMessage( 'badlogin' ) ) .
171                                 $this->getForm() .
172                                 "</div>\n" );
173                 }
174                 return true;
175         }
176         
177         /**
178          * When a bad login attempt is made, increment an expiring counter
179          * in the memcache cloud. Later checks for this may trigger a
180          * captcha display to prevent too many hits from the same place.
181          * @param User $user
182          * @param string $password
183          * @param int $retval authentication return value
184          * @return bool true to keep running callbacks
185          */
186         function triggerUserLogin( $user, $password, $retval ) {
187                 global $wgCaptchaTriggers, $wgCaptchaBadLoginExpiration, $wgMemc;
188                 if( $retval == LoginForm::WRONG_PASS && $wgCaptchaTriggers['badlogin'] ) {
189                         $key = $this->badLoginKey();
190                         $count = $wgMemc->get( $key );
191                         if( !$count ) {
192                                 $wgMemc->add( $key, 0, $wgCaptchaBadLoginExpiration );
193                         }
194                         $count = $wgMemc->incr( $key );
195                 }
196                 return true;
197         }
198         
199         /**
200          * Check if a bad login has already been registered for this
201          * IP address. If so, require a captcha.
202          * @return bool
203          * @access private
204          */
205         function isBadLoginTriggered() {
206                 global $wgMemc;
207                 return intval( $wgMemc->get( $this->badLoginKey() ) ) > 0;
208         }
209         
210         /**
211          * Internal cache key for badlogin checks.
212          * @return string
213          * @access private
214          */
215         function badLoginKey() {
216                 return wfMemcKey( 'captcha', 'badlogin', 'ip', wfGetIP() );
217         }
218         
219         /**
220          * Check if the submitted form matches the captcha session data provided
221          * by the plugin when the form was generated.
222          *
223          * Override this!
224          *
225          * @param string $answer
226          * @param array $info
227          * @return bool
228          */
229         function keyMatch( $answer, $info ) {
230                 return $answer == $info['answer'];
231         }
232
233         // ----------------------------------
234
235         /**
236          * @param EditPage $editPage
237          * @param string $action (edit/create/addurl...)
238          * @return bool true if action triggers captcha on editPage's namespace
239          */
240         function captchaTriggers( &$editPage, $action) {
241                 global $wgCaptchaTriggers, $wgCaptchaTriggersOnNamespace;       
242                 //Special config for this NS?
243                 if (isset( $wgCaptchaTriggersOnNamespace[$editPage->mTitle->getNamespace()][$action] ) )
244                         return $wgCaptchaTriggersOnNamespace[$editPage->mTitle->getNamespace()][$action];
245
246                 return ( !empty( $wgCaptchaTriggers[$action] ) ); //Default
247         }
248
249
250         /**
251          * @param EditPage $editPage
252          * @param string $newtext
253          * @param string $section
254          * @return bool true if the captcha should run
255          */
256         function shouldCheck( &$editPage, $newtext, $section, $merged = false ) {
257                 $this->trigger = '';
258                 $title = $editPage->mArticle->getTitle();
259
260                 global $wgUser;
261                 if( $wgUser->isAllowed( 'skipcaptcha' ) ) {
262                         wfDebug( "ConfirmEdit: user group allows skipping captcha\n" );
263                         return false;
264                 }
265                 global $wgCaptchaWhitelistIP;
266                 if( !empty( $wgCaptchaWhitelistIP ) ) {
267                         $ip = wfGetIp();
268                         foreach ( $wgCaptchaWhitelistIP as $range ) {
269                                 if ( IP::isInRange( $ip, $range ) ) {
270                                         return false;
271                                 }
272                         }
273                 }
274
275
276                 global $wgEmailAuthentication, $ceAllowConfirmedEmail;
277                 if( $wgEmailAuthentication && $ceAllowConfirmedEmail &&
278                         $wgUser->isEmailConfirmed() ) {
279                         wfDebug( "ConfirmEdit: user has confirmed mail, skipping captcha\n" );
280                         return false;
281                 }
282
283                 if( $this->captchaTriggers( $editPage, 'edit' ) ) {
284                         // Check on all edits
285                         global $wgUser;
286                         $this->trigger = sprintf( "edit trigger by '%s' at [[%s]]",
287                                 $wgUser->getName(),
288                                 $title->getPrefixedText() );
289                         $this->action = 'edit';
290                         wfDebug( "ConfirmEdit: checking all edits...\n" );
291                         return true;
292                 }
293
294                 if( $this->captchaTriggers( $editPage, 'create' )  && !$editPage->mTitle->exists() ) {
295                         //Check if creating a page
296                         global $wgUser;
297                         $this->trigger = sprintf( "Create trigger by '%s' at [[%s]]",
298                                 $wgUser->getName(),
299                                 $title->getPrefixedText() );
300                         $this->action = 'create';
301                         wfDebug( "ConfirmEdit: checking on page creation...\n" );
302                         return true;
303                 }
304
305                 if( $this->captchaTriggers( $editPage, 'addurl' ) ) {
306                         // Only check edits that add URLs
307                         if ( $merged ) {
308                                 // Get links from the database
309                                 $oldLinks = $this->getLinksFromTracker( $title );
310                                 // Share a parse operation with Article::doEdit()
311                                 $editInfo = $editPage->mArticle->prepareTextForEdit( $newtext );
312                                 $newLinks = array_keys( $editInfo->output->getExternalLinks() );
313                         } else {
314                                 // Get link changes in the slowest way known to man
315                                 $oldtext = $this->loadText( $editPage, $section );
316                                 $oldLinks = $this->findLinks( $oldtext );
317                                 $newLinks = $this->findLinks( $newtext );
318                         }
319
320                         $unknownLinks = array_filter( $newLinks, array( &$this, 'filterLink' ) );
321                         $addedLinks = array_diff( $unknownLinks, $oldLinks );
322                         $numLinks = count( $addedLinks );
323
324                         if( $numLinks > 0 ) {
325                                 global $wgUser;
326                                 $this->trigger = sprintf( "%dx url trigger by '%s' at [[%s]]: %s",
327                                         $numLinks,
328                                         $wgUser->getName(),
329                                         $title->getPrefixedText(),
330                                         implode( ", ", $addedLinks ) );
331                                 $this->action = 'addurl';
332                                 return true;
333                         }
334                 }
335
336                 global $wgCaptchaRegexes;
337                 if( !empty( $wgCaptchaRegexes ) ) {
338                         // Custom regex checks
339                         $oldtext = $this->loadText( $editPage, $section );
340
341                         foreach( $wgCaptchaRegexes as $regex ) {
342                                 $newMatches = array();
343                                 if( preg_match_all( $regex, $newtext, $newMatches ) ) {
344                                         $oldMatches = array();
345                                         preg_match_all( $regex, $oldtext, $oldMatches );
346
347                                         $addedMatches = array_diff( $newMatches[0], $oldMatches[0] );
348
349                                         $numHits = count( $addedMatches );
350                                         if( $numHits > 0 ) {
351                                                 global $wgUser;
352                                                 $this->trigger = sprintf( "%dx %s at [[%s]]: %s",
353                                                         $numHits,
354                                                         $regex,
355                                                         $wgUser->getName(),
356                                                         $title->getPrefixedText(),
357                                                         implode( ", ", $addedMatches ) );
358                                                 $this->action = 'edit';
359                                                 return true;
360                                         }
361                                 }
362                         }
363                 }
364
365                 return false;
366         }
367
368         /**
369          * Filter callback function for URL whitelisting
370          * @param string url to check
371          * @return bool true if unknown, false if whitelisted
372          * @access private
373          */
374         function filterLink( $url ) {
375                 global $wgCaptchaWhitelist;
376                 $source = wfMsgForContent( 'captcha-addurl-whitelist' );
377
378                 $whitelist = wfEmptyMsg( 'captcha-addurl-whitelist', $source ) 
379                         ? false
380                         : $this->buildRegexes( explode( "\n", $source ) );
381
382                 $cwl = $wgCaptchaWhitelist !== false ? preg_match( $wgCaptchaWhitelist, $url ) : false;
383                 $wl  = $whitelist          !== false ? preg_match( $whitelist, $url )          : false;
384
385                 return !( $cwl || $wl );
386         }
387
388         /**
389          * Build regex from whitelist
390          * @param string lines from [[MediaWiki:Captcha-addurl-whitelist]]
391          * @return string Regex or bool false if whitelist is empty
392          * @access private
393          */
394         function buildRegexes( $lines ) {
395                 # Code duplicated from the SpamBlacklist extension (r19197)
396
397                 # Strip comments and whitespace, then remove blanks
398                 $lines = array_filter( array_map( 'trim', preg_replace( '/#.*$/', '', $lines ) ) );
399
400                 # No lines, don't make a regex which will match everything
401                 if ( count( $lines ) == 0 ) {
402                         wfDebug( "No lines\n" );
403                         return false;
404                 } else {
405                         # Make regex
406                         # It's faster using the S modifier even though it will usually only be run once
407                         //$regex = 'http://+[a-z0-9_\-.]*(' . implode( '|', $lines ) . ')';
408                         //return '/' . str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $regex) ) . '/Si';
409                         $regexes = '';
410                         $regexStart = '/http:\/\/+[a-z0-9_\-.]*(';
411                         $regexEnd = ')/Si';
412                         $regexMax = 4096;
413                         $build = false;
414                         foreach( $lines as $line ) {
415                                 // FIXME: not very robust size check, but should work. :)
416                                 if( $build === false ) {
417                                         $build = $line;
418                                 } elseif( strlen( $build ) + strlen( $line ) > $regexMax ) {
419                                         $regexes .= $regexStart .
420                                                 str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $build) ) .
421                                                 $regexEnd;
422                                         $build = $line;
423                                 } else {
424                                         $build .= '|' . $line;
425                                 }
426                         }
427                         if( $build !== false ) {
428                                 $regexes .= $regexStart .
429                                         str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $build) ) .
430                                         $regexEnd;
431                         }
432                         return $regexes;
433                 }
434         }
435
436         /**
437          * Load external links from the externallinks table
438          */
439         function getLinksFromTracker( $title ) {
440                 $dbr =& wfGetDB( DB_SLAVE );
441                 $id = $title->getArticleId(); // should be zero queries
442                 $res = $dbr->select( 'externallinks', array( 'el_to' ), 
443                         array( 'el_from' => $id ), __METHOD__ );
444                 $links = array();
445                 while ( $row = $dbr->fetchObject( $res ) ) {
446                         $links[] = $row->el_to;
447                 }
448                 return $links;
449         }
450         
451         /**
452          * Backend function for confirmEdit() and confirmEditAPI()
453          * @return bool false if the CAPTCHA is rejected, true otherwise
454          */
455         private function doConfirmEdit( &$editPage, $newtext, $section, $merged = false ) {
456                 if( $this->shouldCheck( $editPage, $newtext, $section, $merged ) ) {
457                         if( $this->passCaptcha() ) {
458                                 return true;
459                         } else {
460                                 return false;
461                         }
462                 } else {
463                         wfDebug( "ConfirmEdit: no need to show captcha.\n" );
464                         return true;
465                 }
466         }
467
468         /**
469          * The main callback run on edit attempts.
470          * @param EditPage $editPage
471          * @param string $newtext
472          * @param string $section
473          * @param bool $merged
474          * @return bool true to continue saving, false to abort and show a captcha form
475          */
476         function confirmEdit( &$editPage, $newtext, $section, $merged = false ) {
477                 global $wgTitle;
478                 if( is_null( $wgTitle ) ) {
479                         # API mode
480                         # The CAPTCHA was already checked and approved 
481                         return true;
482                 }
483                 if( !$this->doConfirmEdit( $editPage, $newtext, $section, $merged ) ) {
484                         $editPage->showEditForm( array( &$this, 'editCallback' ) );
485                         return false;
486                 }
487                 return true;
488         }
489
490         /**
491          * A more efficient edit filter callback based on the text after section merging
492          * @param EditPage $editPage
493          * @param string $newtext
494          */
495         function confirmEditMerged( &$editPage, $newtext ) {
496                 return $this->confirmEdit( $editPage, $newtext, false, true );
497         }
498         
499         
500         function confirmEditAPI( &$editPage, $newtext, &$resultArr) {
501                 if( !$this->doConfirmEdit( $editPage, $newtext, false, false ) ) {
502                         $this->addCaptchaAPI($resultArr);
503                         return false;
504                 }
505                 return true;
506         }
507
508         /**
509          * Hook for user creation form submissions.
510          * @param User $u
511          * @param string $message
512          * @return bool true to continue, false to abort user creation
513          */
514         function confirmUserCreate( $u, &$message ) {
515                 global $wgCaptchaTriggers;
516                 if( $wgCaptchaTriggers['createaccount'] ) {
517                         $this->trigger = "new account '" . $u->getName() . "'";
518                         if( !$this->passCaptcha() ) {
519                                 $message = wfMsg( 'captcha-createaccount-fail' );
520                                 return false;
521                         }
522                 }
523                 return true;
524         }
525         
526         /**
527          * Hook for user login form submissions.
528          * @param User $u
529          * @param string $message
530          * @return bool true to continue, false to abort user creation
531          */
532         function confirmUserLogin( $u, $pass, &$retval ) {
533                 if( $this->isBadLoginTriggered() ) {
534                         $this->trigger = "post-badlogin login '" . $u->getName() . "'";
535                         if( !$this->passCaptcha() ) {
536                                 $message = wfMsg( 'captcha-badlogin-fail' );
537                                 // Emulate a bad-password return to confuse the shit out of attackers
538                                 $retval = LoginForm::WRONG_PASS;
539                                 return false;
540                         }
541                 }
542                 return true;
543         }
544
545         /**
546          * Given a required captcha run, test form input for correct
547          * input on the open session.
548          * @return bool if passed, false if failed or new session
549          */
550         function passCaptcha() {
551                 $info = $this->retrieveCaptcha();
552                 if( $info ) {
553                         global $wgRequest;
554                         if( $this->keyMatch( $wgRequest->getVal('wpCaptchaWord'), $info ) ) {
555                                 $this->log( "passed" );
556                                 $this->clearCaptcha( $info );
557                                 return true;
558                         } else {
559                                 $this->clearCaptcha( $info );
560                                 $this->log( "bad form input" );
561                                 return false;
562                         }
563                 } else {
564                         $this->log( "new captcha session" );
565                         return false;
566                 }
567         }
568
569         /**
570          * Log the status and any triggering info for debugging or statistics
571          * @param string $message
572          */
573         function log( $message ) {
574                 wfDebugLog( 'captcha', 'ConfirmEdit: ' . $message . '; ' .  $this->trigger );
575         }
576
577         /**
578          * Generate a captcha session ID and save the info in PHP's session storage.
579          * (Requires the user to have cookies enabled to get through the captcha.)
580          *
581          * A random ID is used so legit users can make edits in multiple tabs or
582          * windows without being unnecessarily hobbled by a serial order requirement.
583          * Pass the returned id value into the edit form as wpCaptchaId.
584          *
585          * @param array $info data to store
586          * @return string captcha ID key
587          */
588         function storeCaptcha( $info ) {
589                 if( !isset( $info['index'] ) ) {
590                         // Assign random index if we're not udpating
591                         $info['index'] = strval( mt_rand() );
592                 }
593                 $this->storage->store( $info['index'], $info );
594                 return $info['index'];
595         }
596
597         /**
598          * Fetch this session's captcha info.
599          * @return mixed array of info, or false if missing
600          */
601         function retrieveCaptcha() {
602                 global $wgRequest;
603                 $index = $wgRequest->getVal( 'wpCaptchaId' );
604                 return $this->storage->retrieve( $index );
605         }
606
607         /**
608          * Clear out existing captcha info from the session, to ensure
609          * it can't be reused.
610          */
611         function clearCaptcha( $info ) {
612                 $this->storage->clear( $info['index'] );
613         }
614
615         /**
616          * Retrieve the current version of the page or section being edited...
617          * @param EditPage $editPage
618          * @param string $section
619          * @return string
620          * @access private
621          */
622         function loadText( $editPage, $section ) {
623                 $rev = Revision::newFromTitle( $editPage->mTitle );
624                 if( is_null( $rev ) ) {
625                         return "";
626                 } else {
627                         $text = $rev->getText();
628                         if( $section != '' ) {
629                                 return Article::getSection( $text, $section );
630                         } else {
631                                 return $text;
632                         }
633                 }
634         }
635
636         /**
637          * Extract a list of all recognized HTTP links in the text.
638          * @param string $text
639          * @return array of strings
640          */
641         function findLinks( $text ) {
642                 global $wgParser, $wgTitle, $wgUser;
643
644                 $options = new ParserOptions();
645                 $text = $wgParser->preSaveTransform( $text, $wgTitle, $wgUser, $options );
646                 $out = $wgParser->parse( $text, $wgTitle, $options );
647
648                 return array_keys( $out->getExternalLinks() );
649         }
650
651         /**
652          * Show a page explaining what this wacky thing is.
653          */
654         function showHelp() {
655                 global $wgOut, $ceAllowConfirmedEmail;
656                 $wgOut->setPageTitle( wfMsg( 'captchahelp-title' ) );
657                 $wgOut->addWikiText( wfMsg( 'captchahelp-text' ) );
658                 if ( $this->storage->cookiesNeeded() ) {
659                         $wgOut->addWikiText( wfMsg( 'captchahelp-cookies-needed' ) );
660                 }
661         }
662
663 }
664
665 class CaptchaSessionStore {
666         function store( $index, $info ) {
667                 $_SESSION['captcha' . $info['index']] = $info;
668         }
669         
670         function retrieve( $index ) {
671                 if( isset( $_SESSION['captcha' . $index] ) ) {
672                         return $_SESSION['captcha' . $index];
673                 } else {
674                         return false;
675                 }
676         }
677         
678         function clear( $index ) {
679                 unset( $_SESSION['captcha' . $index] );
680         }
681
682         function cookiesNeeded() {
683                 return true;
684         }
685 }
686
687 class CaptchaCacheStore {
688         function store( $index, $info ) {
689                 global $wgMemc, $wgCaptchaSessionExpiration;
690                 $wgMemc->set( wfMemcKey( 'captcha', $index ), $info,
691                         $wgCaptchaSessionExpiration );
692         }
693
694         function retrieve( $index ) {
695                 global $wgMemc;
696                 $info = $wgMemc->get( wfMemcKey( 'captcha', $index ) );
697                 if( $info ) {
698                         return $info;
699                 } else {
700                         return false;
701                 }
702         }
703         
704         function clear( $index ) {
705                 global $wgMemc;
706                 $wgMemc->delete( wfMemcKey( 'captcha', $index ) );
707         }
708
709         function cookiesNeeded() {
710                 return false;
711         }
712 }
713