Modifying ConfirmEdit extension to make it API-friendly:
[toast/cookiecaptcha.git] / FancyCaptcha.class.php
1 <?php
2
3 class FancyCaptcha extends SimpleCaptcha {
4         /**
5          * Check if the submitted form matches the captcha session data provided
6          * by the plugin when the form was generated.
7          *
8          * @param string $answer
9          * @param array $info
10          * @return bool
11          */
12         function keyMatch( $answer, $info ) {
13                 global $wgCaptchaSecret;
14
15                 $digest = $wgCaptchaSecret . $info['salt'] . $answer . $wgCaptchaSecret . $info['salt'];
16                 $answerHash = substr( md5( $digest ), 0, 16 );
17
18                 if( $answerHash == $info['hash'] ) {
19                         wfDebug( "FancyCaptcha: answer hash matches expected {$info['hash']}\n" );
20                         return true;
21                 } else {
22                         wfDebug( "FancyCaptcha: answer hashes to $answerHash, expected {$info['hash']}\n" );
23                         return false;
24                 }
25         }
26
27         function addCaptchaAPI(&$resultArr) {
28                 $info = $this->pickImage();
29                 if( !$info ) {
30                         $resultArr['captcha']['error'] = 'Out of images';
31                         return;
32                 }
33                 $index = $this->storeCaptcha( $info );
34                 $title = Title::makeTitle( NS_SPECIAL, 'Captcha/image' );
35                 $resultArr['captcha']['type'] = 'image';
36                 $resultArr['captcha']['id'] = $index;
37                 $resultArr['captcha']['url'] = $title->getLocalUrl( 'wpCaptchaId=' . urlencode( $index ) );             
38         }
39
40         /**
41          * Insert the captcha prompt into the edit form.
42          */
43         function getForm() {
44                 $info = $this->pickImage();
45                 if( !$info ) {
46                         die( "out of captcha images; this shouldn't happen" );
47                 }
48
49                 // Generate a random key for use of this captcha image in this session.
50                 // This is needed so multiple edits in separate tabs or windows can
51                 // go through without extra pain.
52                 $index = $this->storeCaptcha( $info );
53
54                 wfDebug( "Captcha id $index using hash ${info['hash']}, salt ${info['salt']}.\n" );
55
56                 $title = Title::makeTitle( NS_SPECIAL, 'Captcha/image' );
57
58                 return "<p>" .
59                         wfElement( 'img', array(
60                                 'src'    => $title->getLocalUrl( 'wpCaptchaId=' . urlencode( $index ) ),
61                                 'width'  => $info['width'],
62                                 'height' => $info['height'],
63                                 'alt'    => '' ) ) .
64                         "</p>\n" .
65                         wfElement( 'input', array(
66                                 'type'  => 'hidden',
67                                 'name'  => 'wpCaptchaId',
68                                 'id'    => 'wpCaptchaId',
69                                 'value' => $index ) ) .
70                         "<p>" .
71                         wfElement( 'input', array(
72                                 'name' => 'wpCaptchaWord',
73                                 'id'   => 'wpCaptchaWord',
74                                 'tabindex' => 1 ) ) . // tab in before the edit textarea
75                         "</p>\n";
76         }
77
78         /**
79          * Select a previously generated captcha image from the queue.
80          * @fixme subject to race conditions if lots of files vanish
81          * @return mixed tuple of (salt key, text hash) or false if no image to find
82          */
83         function pickImage() {
84                 global $wgCaptchaDirectory, $wgCaptchaDirectoryLevels;
85                 return $this->pickImageDir(
86                         $wgCaptchaDirectory,
87                         $wgCaptchaDirectoryLevels );
88         }
89         
90         function pickImageDir( $directory, $levels ) {
91                 if( $levels ) {
92                         $dirs = array();
93                         
94                         // Check which subdirs are actually present...
95                         $dir = opendir( $directory );
96                         while( false !== ($entry = readdir( $dir ) ) ) {
97                                 if( ctype_xdigit( $entry ) && strlen( $entry ) == 1 ) {
98                                         $dirs[] = $entry;
99                                 }
100                         }
101                         closedir( $dir );
102                         
103                         $place = mt_rand( 0, count( $dirs ) - 1 );
104                         // In case all dirs are not filled,
105                         // cycle through next digits...
106                         for( $j = 0; $j < count( $dirs ); $j++ ) {
107                                 $char = $dirs[($place + $j) % count( $dirs )];
108                                 $return = $this->pickImageDir( "$directory/$char", $levels - 1 );
109                                 if( $return ) {
110                                         return $return;
111                                 }
112                         }
113                         // Didn't find any images in this directory... empty?
114                         return false;
115                 } else {
116                         return $this->pickImageFromDir( $directory );
117                 }
118         }
119         
120         function pickImageFromDir( $directory ) {
121                 if( !is_dir( $directory ) ) {
122                         return false;
123                 }
124                 $n = mt_rand( 0, $this->countFiles( $directory ) - 1 );
125                 $dir = opendir( $directory );
126
127                 $count = 0;
128
129                 $entry = readdir( $dir );
130                 $pick = false;
131                 while( false !== $entry ) {
132                         $entry = readdir( $dir );
133                         if( preg_match( '/^image_([0-9a-f]+)_([0-9a-f]+)\\.png$/', $entry, $matches ) ) {
134                                 $size = getimagesize( "$directory/$entry" );
135                                 $pick = array(
136                                         'salt' => $matches[1],
137                                         'hash' => $matches[2],
138                                         'width' => $size[0],
139                                         'height' => $size[1],
140                                         'viewed' => false,
141                                 );
142                                 if( $count++ == $n ) {
143                                         break;
144                                 }
145                         }
146                 }
147                 closedir( $dir );
148                 return $pick;
149         }
150
151         /**
152          * Count the number of files in a directory.
153          * @return int
154          */
155         function countFiles( $dirname ) {
156                 $dir = opendir( $dirname );
157                 $count = 0;
158                 while( false !== ($entry = readdir( $dir ) ) ) {
159                         if( $entry != '.' && $entry != '..' ) {
160                                 $count++;
161                         }
162                 }
163                 closedir( $dir );
164                 return $count;
165         }
166
167         function showImage() {
168                 global $wgOut, $wgRequest;
169
170                 $wgOut->disable();
171
172                 $info = $this->retrieveCaptcha();
173                 if( $info ) {
174                         /*
175                         // Be a little less restrictive for now; in at least some circumstances,
176                         // Konqueror tries to reload the image even if you haven't navigated
177                         // away from the page.
178                         if( $info['viewed'] ) {
179                                 wfHttpError( 403, 'Access Forbidden', "Can't view captcha image a second time." );
180                                 return false;
181                         }
182                         */
183
184                         $info['viewed'] = wfTimestamp();
185                         $this->storeCaptcha( $info );
186
187                         $salt = $info['salt'];
188                         $hash = $info['hash'];
189                         $file = $this->imagePath( $salt, $hash );
190
191                         if( file_exists( $file ) ) {
192                                 global $IP;
193                                 require_once "$IP/includes/StreamFile.php";
194                                 header( "Cache-Control: private, s-maxage=0, max-age=3600" );
195                                 wfStreamFile( $file );
196                                 return true;
197                         }
198                 }
199                 wfHttpError( 500, 'Internal Error', 'Requested bogus captcha image' );
200                 return false;
201         }
202         
203         function imagePath( $salt, $hash ) {
204                 global $wgCaptchaDirectory, $wgCaptchaDirectoryLevels;
205                 $file = $wgCaptchaDirectory;
206                 $file .= DIRECTORY_SEPARATOR;
207                 for( $i = 0; $i < $wgCaptchaDirectoryLevels; $i++ ) {
208                         $file .= $hash{$i};
209                         $file .= DIRECTORY_SEPARATOR;
210                 }
211                 $file .= "image_{$salt}_{$hash}.png";
212                 return $file;
213         }
214
215         /**
216          * Show a message asking the user to enter a captcha on edit
217          * The result will be treated as wiki text
218          *
219          * @param $action Action being performed
220          * @return string
221          */
222         function getMessage( $action ) {
223                 $name = 'fancycaptcha-' . $action;
224                 $text = wfMsg( $name );
225                 # Obtain a more tailored message, if possible, otherwise, fall back to
226                 # the default for edits
227                 return wfEmptyMsg( $name, $text ) ? wfMsg( 'fancycaptcha-edit' ) : $text;
228         }
229
230 }