gd_security_image.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. # GdSecurityImage - a CAPTCHA module for Oddmuse using GD::SecurityImage module
  2. #
  3. # Copyright (C) 2014 Aki Goto <tyatsumi@gmail.com>
  4. #
  5. # Codes reused from MwfCaptcha.pm in mwForum - Web-based discussion forum
  6. # Copyright (c) 1999-2014 Markus Wichitill
  7. #
  8. # Codes reused from questionasker.pl for Oddmuse
  9. # Copyright (C) 2004 Brock Wilcox <awwaiid@thelackthereof.org>
  10. # Copyright (C) 2006–2015 Alex Schroeder <alex@gnu.org>
  11. #
  12. # This program is free software: you can redistribute it and/or modify
  13. # it under the terms of the GNU General Public License as published by
  14. # the Free Software Foundation, either version 3 of the License, or
  15. # (at your option) any later version.
  16. #
  17. # This program is distributed in the hope that it will be useful,
  18. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. # GNU General Public License for more details.
  21. #
  22. # You should have received a copy of the GNU General Public License
  23. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. use strict;
  25. use v5.10;
  26. AddModuleDescription('gd_security_image.pl', 'GD Security Image Extension');
  27. our ($q, $Now, %Action, $FullUrl, $LinkPattern, $FreeLinks, $FreeLinkPattern, $WikiLinks, $DataDir, $ModuleDir, @MyInitVariables, %CookieParameters, @MyFormChanges);
  28. =head1 DESCRIPTION
  29. This is a CAPTCHA module for Oddmuse using GD::SecurityImage module.
  30. =head1 CONFIGURATION
  31. $GdSecurityImageFont
  32. Mandatory.
  33. Set a TTF font file used for generating CAPTCHA images.
  34. Example: '/usr/share/fonts/truetype/ttf-bitstream-vera/VeraBd.ttf'.
  35. $GdSecurityImageRememberAnswer
  36. If 1, once CAPTCHA is answered, the result is cached on cookies
  37. and you need not to re-answer CAPTCHAs for some duration specified
  38. by $GdSecurityImageDuration.
  39. If 0, CAPTCHA is requested everytime you try to submit forms.
  40. Default = 1.
  41. $GdSecurityImageDuration
  42. The duration a CAPTCHA ticket is valid in seconds.
  43. Default = 60 * 10 (10 minutes).
  44. $GdSecurityImageRequiredList
  45. The page name for exceptions, if defined. Every page linked to via
  46. WikiWord or [[free link]] is considered to be a page which needs
  47. questions asked. All other pages do not require questions asked. If
  48. not set, then all pages need questions asked.
  49. %GdSecurityImageProtectedForms
  50. Forms using one of the specified classes are protected.
  51. Default: ('comment' => 1, 'edit upload' => 1, 'edit text' => 1,).
  52. $GdSecurityImageDataDir
  53. When using with Namespaces Extension, specify original root data directory
  54. to concentrate GdSecurityImage data files in it.
  55. Default: $DataDir.
  56. $GdSecurityImageWidth
  57. Default: 250.
  58. $GdSecurityImageHeight
  59. Default: 60.
  60. $GdSecurityImagePtsize
  61. Default: 16.
  62. $GdSecurityImageScramble
  63. Default: 1.
  64. $GdSecurityImageChars
  65. Default: [qw(A B C D E F G H I J K L M O P R S T U V W X Y)].
  66. =head1 API
  67. You can use this module in other modules by using following APIs.
  68. GdSecurityImageGetHtml
  69. returns CAPTCHA HTML form element for embedding in HTML form clause.
  70. GdSecurityImageCheck
  71. returns whether CAPTCHA is answered correctly or not.
  72. =head1 DATA STRUCTURE
  73. Image data and ticket data are stored in $DataDir/gd_security_image directory.
  74. Old data are deleted partially whenever CAPTCHA form is accessed.
  75. You can delete this directory totally harmlessly, although it forces users to
  76. re-answer CAPTCHA.
  77. =cut
  78. our ($GdSecurityImageFont, $GdSecurityImageRememberAnswer,
  79. $GdSecurityImageDuration, $GdSecurityImageRequiredList,
  80. %GdSecurityImageProtectedForms, $GdSecurityImageDataDir,
  81. $GdSecurityImageWidth, $GdSecurityImageHeight,
  82. $GdSecurityImagePtsize, $GdSecurityImageScramble, $GdSecurityImageChars,
  83. $GdSecurityImageAA);
  84. our ($GdSecurityImageDir, $GdSecurityImageId, $GdSecurityImagePngToAA);
  85. use Digest::MD5;
  86. use File::Glob ':glob';
  87. $GdSecurityImageRequiredList = '';
  88. $Action{gd_security_image} = \&GdSecurityImageDoImage;
  89. push(@MyInitVariables, \&GdSecurityImageInitVariables);
  90. sub GdSecurityImageGetImageFile {
  91. my ($id) = @_;
  92. return "$GdSecurityImageDir/$id.png";
  93. }
  94. sub GdSecurityImageGetTicketFile {
  95. my ($id) = @_;
  96. return "$GdSecurityImageDir/$id.ticket";
  97. }
  98. sub GdSecurityImageGenerate {
  99. # Load modules
  100. my $gd = eval { require GD };
  101. eval { require Image::Magick }
  102. or ReportError(T('GD or Image::Magick modules not available.'), '500 INTERNAL SERVER ERROR') if !$gd;
  103. eval { require GD::SecurityImage }
  104. or ReportError(T('GD::SecurityImage module not available.'));
  105. # Generate captcha image
  106. GD::SecurityImage->import($gd ? () : (use_magick => 1));
  107. my $img = GD::SecurityImage->new(
  108. width => $GdSecurityImageWidth,
  109. height => $GdSecurityImageHeight,
  110. font => $GdSecurityImageFont,
  111. ptsize => $GdSecurityImagePtsize,
  112. scramble => $GdSecurityImageScramble,
  113. rnd_data => $GdSecurityImageChars,
  114. bgcolor => '#000000',
  115. );
  116. $img->random();
  117. my $newCaptchaStr = $img->random_str();
  118. $img->create('ttf', int(rand(2)) ? 'default' : 'ec', '#ffffff', '#ffffff');
  119. $img->particle(3000);
  120. ### experimental ###
  121. #my $raw = $img->raw;
  122. #my $w2 = $GdSecurityImageWidth * 2 / 3;
  123. #my $h2 = $GdSecurityImageHeight * 2 / 3;
  124. #my $raw2 = GD::Image->new($w2, $h2);
  125. #$raw2->copyResampled($raw, 0, 0, 0, 0, $w2, $h2, $raw->getBounds);
  126. #my $png = $raw2->png;
  127. # Store captcha image
  128. my ($imgData) = $img->out(force => 'png');
  129. my $ticketId = Digest::MD5::md5_hex(rand());
  130. CreateDir($GdSecurityImageDir);
  131. open my $fh, ">:raw", encode_utf8(GdSecurityImageGetImageFile($ticketId))
  132. or ReportError(Ts('Image storing failed. (%s)', $!), '500 INTERNAL SERVER ERROR');
  133. print $fh $imgData;
  134. #print $fh $png; ### experimental ###
  135. close $fh;
  136. # Insert captcha ticket
  137. my %page = ();
  138. $page{id} = $ticketId;
  139. $page{generation_time} = $Now;
  140. $page{string} = $newCaptchaStr;
  141. CreateDir($GdSecurityImageDir);
  142. WriteStringToFile(GdSecurityImageGetTicketFile($ticketId), EncodePage(%page));
  143. return $ticketId;
  144. }
  145. sub GdSecurityImageIsValidId {
  146. my ($id) = @_;
  147. return $id =~ /^[0-9a-f]+$/;
  148. }
  149. sub GdSecurityImageReadImageFile {
  150. if (open(my $IN, '<:raw', encode_utf8(shift))) {
  151. local $/ = undef; # Read complete files
  152. my $data=<$IN>;
  153. close $IN;
  154. return (1, $data);
  155. }
  156. return (0, '');
  157. }
  158. sub GdSecurityImageDoImage {
  159. my $id = GetParam('gd_security_image_id', '');
  160. if (!GdSecurityImageIsValidId($id)) {
  161. ReportError(T('Bad gd_security_image_id.'), '400 BAD REQUEST');
  162. }
  163. my ($status, $data) = GdSecurityImageReadImageFile(GdSecurityImageGetImageFile($id));
  164. binmode(STDOUT, ":raw");
  165. print $q->header(-type=>'image/png');
  166. print $data;
  167. Unlink(GdSecurityImageGetImageFile($id));
  168. }
  169. sub GdSecurityImageCleanup {
  170. my ($id) = @_;
  171. if (!GdSecurityImageIsValidId($id)) {
  172. return;
  173. }
  174. my @files = (Glob("$GdSecurityImageDir/*.png"), Glob("$GdSecurityImageDir/*.ticket"));
  175. foreach my $file (@files) {
  176. if ($Now - Modified($file) > $GdSecurityImageDuration) {
  177. Unlink($file);
  178. }
  179. }
  180. }
  181. sub GdSecurityImageCheck {
  182. if (defined($GdSecurityImageId)) {
  183. return $GdSecurityImageId eq '';
  184. }
  185. my $id = GetParam('gd_security_image_id', '');
  186. my $answer = GetParam('gd_security_image_answer', '');
  187. GdSecurityImageCleanup($id);
  188. if ($answer ne '' && GdSecurityImageIsValidId($id)) {
  189. my ($status, $data) = ReadFile(GdSecurityImageGetTicketFile($id));
  190. if ($status) {
  191. my $page = ParseData($data);
  192. if ($page->{generation_time} + $GdSecurityImageDuration > $Now) {
  193. if ($answer eq $page->{string}) {
  194. $GdSecurityImageId = '';
  195. if (!$GdSecurityImageRememberAnswer) {
  196. SetParam('gd_security_image_id', '');
  197. SetParam('gd_security_image_answer', '');
  198. }
  199. return 1;
  200. }
  201. }
  202. }
  203. }
  204. if (GdSecurityImageIsValidId($id)) {
  205. Unlink(GdSecurityImageGetTicketFile($id));
  206. }
  207. $GdSecurityImageId = GdSecurityImageGenerate();
  208. return 0;
  209. }
  210. sub GdSecurityImageGetHtml {
  211. if (GdSecurityImageCheck()) {
  212. return '';
  213. }
  214. my $form = '';
  215. SetParam('gd_security_image_answer', '');
  216. $form .= $q->start_div({-class=>'gd_security_image'});
  217. $form .= $q->start_div();
  218. $form .= T('Please type the six characters from the anti-spam image');
  219. $form .= $q->end_div();
  220. $form .= $q->start_div();
  221. $form .= $q->input({-type=>'hidden', -name=>'gd_security_image_id', -value=>$GdSecurityImageId});
  222. $form .= $q->textfield(-name=>'gd_security_image_answer', -id=>'gd_security_image_answer');
  223. $form .= $q->submit(-name=>'Submit', -value=>T('Submit'));
  224. $form .= $q->end_div();
  225. $form .= $q->start_div();
  226. $form .= $q->img({-src=>"$FullUrl?action=gd_security_image&gd_security_image_id=$GdSecurityImageId", -alt=>T('CAPTCHA'), -width=>$GdSecurityImageWidth, -height=>$GdSecurityImageHeight});
  227. $form .= $q->end_div();
  228. if ($GdSecurityImageAA) {
  229. $form .= $q->start_div({class=>'aa_captcha'});
  230. $form .= $q->start_pre();
  231. my $png_file = GdSecurityImageGetImageFile($GdSecurityImageId);
  232. $form .= `$GdSecurityImagePngToAA $png_file`;
  233. $form .= $q->end_pre();
  234. $form .= $q->end_div();
  235. }
  236. $form .= $q->end_div();
  237. return $form;
  238. }
  239. *OldGdSecurityImageDoPost = \&DoPost;
  240. *DoPost = \&NewGdSecurityImageDoPost;
  241. sub NewGdSecurityImageDoPost {
  242. my(@params) = @_;
  243. my $id = FreeToNormal(GetParam('title', undef));
  244. my $preview = GetParam('Preview', undef); # case matters!
  245. unless (UserIsEditor()
  246. or $preview
  247. or GdSecurityImageCheck()
  248. or GdSecurityImageException($id)) {
  249. print GetHeader('', T('Edit Denied'), undef, undef, '403 FORBIDDEN');
  250. print $q->p(T('You did not answer correctly.'));
  251. print GetFormStart(), GdSecurityImageGetHtml(),
  252. (map { $q->input({-type=>'hidden', -name=>$_, -value=>UnquoteHtml(GetParam($_))}) }
  253. qw(title text oldtime summary recent_edit aftertext)), $q->end_form;
  254. PrintFooter();
  255. # logging to the error log file of the server
  256. # warn "Q: '$QuestionaskerQuestions[$question_num][0]', A: '$answer'\n";
  257. return;
  258. }
  259. return (OldGdSecurityImageDoPost(@params));
  260. }
  261. push(@MyFormChanges, \&GdSecurityImageAddTo);
  262. sub GdSecurityImageAddTo {
  263. my ($form, $type, $upload) = @_;
  264. if (not $upload
  265. and not GdSecurityImageException(GetId())
  266. and not UserIsEditor()) {
  267. my $question = GdSecurityImageGetHtml();
  268. $form =~ s/(.*)<p>(.*?)<label for="username">/$1$question<p>$2<label for="username">/;
  269. }
  270. return $form;
  271. }
  272. sub GdSecurityImageException {
  273. my $id = shift;
  274. return 0 unless $GdSecurityImageRequiredList and $id;
  275. my $data = GetPageContent($GdSecurityImageRequiredList);
  276. if ($WikiLinks) {
  277. while ($data =~ /$LinkPattern/g) {
  278. return 0 if FreeToNormal($1) eq $id;
  279. }
  280. }
  281. if ($FreeLinks) {
  282. while ($data =~ /\[\[$FreeLinkPattern\]\]/g) {
  283. return 0 if FreeToNormal($1) eq $id;
  284. }
  285. }
  286. return 1;
  287. }
  288. sub GdSecurityImageInitVariables {
  289. ReportError(T('$GdSecurityImageFont is not set.'), '500 INTERNAL SERVER ERROR') unless defined $GdSecurityImageFont;
  290. $GdSecurityImageRememberAnswer = 1 unless defined $GdSecurityImageRememberAnswer;
  291. $GdSecurityImageDuration = 60 * 10 unless defined $GdSecurityImageDuration;
  292. $GdSecurityImageRequiredList = FreeToNormal($GdSecurityImageRequiredList);
  293. # Forms using one of the following classes are protected.
  294. %GdSecurityImageProtectedForms = ('comment' => 1,
  295. 'edit upload' => 1,
  296. 'edit text' => 1,)
  297. unless %GdSecurityImageProtectedForms;
  298. $GdSecurityImageDataDir = $DataDir unless defined $GdSecurityImageDataDir;
  299. $GdSecurityImageWidth = 240 unless defined $GdSecurityImageWidth;
  300. $GdSecurityImageHeight = 75 unless defined $GdSecurityImageHeight;
  301. $GdSecurityImagePtsize = 16.75 unless defined $GdSecurityImagePtsize;
  302. $GdSecurityImageScramble = 1 unless defined $GdSecurityImageScramble;
  303. $GdSecurityImageChars = [qw(A B C D E F G H I J K L M O P R S T U V W X Y)] unless defined $GdSecurityImageChars;
  304. $GdSecurityImageAA = 0 unless defined $GdSecurityImageAA;
  305. $GdSecurityImageDir = "$GdSecurityImageDataDir/gd_security_image";
  306. $GdSecurityImageId = undef;
  307. $GdSecurityImagePngToAA = "$ModuleDir/pngtoaa";
  308. $CookieParameters{'gd_security_image_id'} = '';
  309. $CookieParameters{'gd_security_image_answer'} = '';
  310. }