OSDN Git Service

[Add] チケット #37551 までの記述追加
[hengband/web.git] / score / register_score.php
1 <?php
2 //ini_set('display_errors', 'On');
3
4 ini_set('log_errors', 'On');
5 ini_set('error_log', 'errors/'.pathinfo(__FILE__, PATHINFO_FILENAME).'.log');
6
7 require_once "db_common.inc";
8 require_once "dump_file.inc";
9 require_once "feed_maker.inc";
10
11 // 登録が成功しない場合、HTTPレスポンスコード 400 Bad Request を返す
12 http_response_code(400);
13
14 /**
15  * 送信されてきたスコアのマルチバイト文字エンコーディングを取得する
16  *
17  * Content-Typeヘッダからエンコーディングを取得する。
18  * 同時に変愚蛮怒が送ってきているはずのContent-Typeヘッダと一致しているかチェックする。
19  *
20  * @return string|false 送られてきたスコアの、PHPのマルチバイト文字処理における文字エンコーディングを表す文字列
21  *                      取得できなかった場合はFALSE
22  */
23 function get_mb_encoding()
24 {
25     $content_type = filter_input(INPUT_SERVER, 'CONTENT_TYPE');
26     if ($content_type == null) {
27         return false;
28     }
29
30     $s = explode(';', $content_type);
31
32     if (count($s) != 2 ||
33         $s[0] !== "text/plain" ||
34         strpos(trim($s[1]), "charset=") !== 0) {
35         return false;
36     }
37
38     $c = explode('=', $s[1]);
39
40     switch (strtolower(trim($c[1]))) {
41     case "euc-jp":
42         return "EUC-JP";
43     case "shift_jis":
44         return "SJIS";
45     case "utf-8":
46         return "UTF-8";
47     default:
48         return false;
49     }
50 }
51
52
53 /**
54  * 受信したスコアデータを分割する
55  *
56  * 受信したスコアデータを、キャラクタ情報部分・キャラクタダンプ部分・スクリーンショット部分の3つに分割する
57  * それぞれの部分は1行毎の文字列の配列とする
58  *
59  * @param string $recv_contents 受信したスコアデータ
60  * @return array|false キャラクタ情報・キャラクタダンプ・スクリーンショットのそれぞれの部分のデータの配列
61  *                     分割できなかった場合はFALSE
62  */
63 function split_recv_contents($recv_contents)
64 {
65     $recv_lines = explode("\n", $recv_contents);
66
67     $dump_info_end_line = array_search('-----charcter dump-----', $recv_lines);
68     $dump_txt_end_line = array_search('-----screen shot-----', $recv_lines);
69
70     if ($dump_info_end_line === false) {
71         return false;
72     }
73
74     $info_lines = array_slice($recv_lines, 0, $dump_info_end_line);
75     $dump_lines = array_slice(
76         $recv_lines,
77         $dump_info_end_line + 1,
78         $dump_txt_end_line ? $dump_txt_end_line - $dump_info_end_line - 1: null
79     );
80     $screen_lines = $dump_txt_end_line ?
81                   array_slice(
82                       $recv_lines,
83                       $dump_txt_end_line + 1,
84                       null
85                   ) : false;
86
87     return [$info_lines, $dump_lines, $screen_lines];
88 }
89
90
91 /**
92  * キャラクタ情報をパースする
93  *
94  * スコアデータのキャラクタ情報をパースし、情報名=>値の連想配列を得る
95  * ex) name: Hoge を ['name' => 'Hoge'] のようにする
96  *
97  * @param string $info_liens 受信したスコアデータのキャラクタ情報(1要素1行の配列)
98  * @return array|false キャラクタ情報・キャラクタダンプ・スクリーンショットのそれぞれの部分のデータの配列
99  *                     分割できなかった場合はFALSE
100  */
101 function parse_character_info($info_lines)
102 {
103     $info = [];
104     foreach ($info_lines as $l) {
105         $splitpos = strpos($l, ':');
106         if ($splitpos !== false) {
107             $key = substr($l, 0, $splitpos);
108             $val = substr($l, $splitpos + 1);
109             $info[$key] = trim($val);
110         }
111     }
112
113     return $info;
114 }
115
116
117 /**
118  * キャラクタ情報からDBへのスコア登録用パラメータを生成する
119  *
120  * @param array $info キャラクタ情報の連想配列
121  * @return array DBへのスコア登録用パラメータ('character_info'と'realm_info'の連想配列)
122  */
123 function create_db_insert_score_data($info)
124 {
125     $character_info_array = [
126         'version' => $info['version'],
127         'score' => $info['score'],
128         'name' => $info['name'],
129         'race' => $info['race'],
130         'class' => $info['class'],
131         'personality' => $info['seikaku'],
132         'sex' => $info['sex'],
133         'level' => $info['level'],
134         'depth' => $info['depth'],
135         'maxlv' => $info['maxlv'],
136         'maxdp' => $info['maxdp'],
137         'au' => $info['au'],
138         'turns' => $info['turns'],
139         'winner' => $info['killer'] == 'ripe' || $info['killer'] == 'Seppuku',
140         'killer' => $info['killer'],
141     ];
142
143     $realm_info_array = array_values(
144         array_filter(
145             [$info['realm1'], $info['realm2']],
146             function ($v) {
147                 return $v !== '魔法なし';
148             }
149         )
150     );
151
152     return [
153         'character_info' => $character_info_array,
154         'realm_info' => $realm_info_array,
155     ];
156 }
157
158
159 /**
160  * スクリーンダンプのバリデーションを行う
161  *
162  * スクリプト実行などの悪意を持ったスクリーンダンプを登録できないよう、
163  * 使用可能なタグをhtml,body,pre,fontに制限する
164  *
165  * @param array $screen_dump_lines スクリーンダンプの文字列の配列
166  * @return バリデーションに成功したらTRUE、失敗したらFALSE
167  */
168 function validate_screen_dump($screen_dump_lines)
169 {
170     if ($screen_dump_lines === false) {
171         return false;
172     }
173
174     $allow_tags = ['html', 'body', 'pre', 'font'];
175
176     $is_valid = true;
177     foreach ($screen_dump_lines as $line) {
178         if (preg_match_all("|</?([^>\s]+)(\s*[^>]+)?>|", $line, $matches, PREG_SET_ORDER) > 0) {
179             $invalid_tag_matches = array_filter(
180                 $matches,
181                 function ($m) use ($allow_tags) {
182                     return !in_array($m[1], $allow_tags);
183                 }
184             );
185             if (count($invalid_tag_matches) > 0) {
186                 $is_valid = false;
187             }
188         }
189     }
190
191     return $is_valid;
192 }
193
194
195 $recv_encoding = get_mb_encoding();
196 if ($recv_encoding === false) {
197     exit;
198 }
199
200 $recv_contents = file_get_contents('php://input');
201 if (strlen($recv_contents) !== filter_input(INPUT_SERVER, 'CONTENT_LENGTH', FILTER_VALIDATE_INT)) {
202     exit;
203 }
204
205 $recv_contents = mb_convert_encoding($recv_contents, "UTF-8", $recv_encoding);
206
207 $split_contents = split_recv_contents($recv_contents);
208 if ($split_contents === false) {
209     exit;
210 }
211
212 $char_info = parse_character_info($split_contents[0]);
213
214 $db = new ScoreDB();
215 $score_id = $db->register_new_score(create_db_insert_score_data($char_info));
216
217 if ($score_id === false) {
218     exit;
219 }
220
221 // 登録成功、HTTPレスポンスコード 200 OK を返す
222 http_response_code(200);
223
224 $dumpfile = new DumpFile($score_id);
225 $dumpfile->save('dumps', 'txt', $split_contents[1]);
226 if (validate_screen_dump($split_contents[2])) {
227     $dumpfile->save('screens', 'html', $split_contents[2]);
228 } else {
229     $dumpfile->save('screens', 'html.bad', $split_contents[2]);
230 }
231
232 $dead_place = $dumpfile->get_dead_place();
233 $db->update_dead_place($score_id, $dead_place);
234
235 exec("nohup python tools/tweet_score.py -c tools/tweet_score.cfg -l tweet_score.log -s {$score_id} > /dev/null &");
236
237 $feed_maker = new FeedMaker($db);
238 $feed_maker->make_atom_feed("feed/newcome-atom.xml");