1
#!/usr/bin/env php
2
<?php
3
/*
4
 * StatusNet - the distributed open-source microblogging tool
5
 * Copyright (C) 2009, StatusNet, Inc.
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 */
20
21
# Abort if called from a web server
22
23
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
24
25
$helptext = <<<ENDOFHELP
26
fixup_utf8.php <maxdate> <maxid> <minid>
27
28
Fixup records in a database that stored the data incorrectly (pre-0.7.4 for StatusNet).
29
30
ENDOFHELP;
31
32
require_once INSTALLDIR.'/scripts/commandline.inc';
33
require_once 'DB.php';
34
35
class UTF8FixerUpper
36
{
37
    var $dbl = null;
38
    var $dbu = null;
39
    var $args = array();
40
41
    function __construct($args)
42
    {
43
        $this->args = $args;
44
45
        if (!empty($args['max_date'])) {
46
            $this->max_date = strftime('%Y-%m-%d %H:%M:%S', strtotime($args['max_date']));
47
        } else {
48
            $this->max_date = strftime('%Y-%m-%d %H:%M:%S', time());
49
        }
50
51
        $this->dbl = $this->doConnect('latin1');
52
53
        if (empty($this->dbl)) {
54
            return;
55
        }
56
57
        $this->dbu = $this->doConnect('utf8');
58
59
        if (empty($this->dbu)) {
60
            return;
61
        }
62
    }
63
64
    function doConnect($charset)
65
    {
66
        $db = DB::connect(common_config('db', 'database'),
67
                          array('persistent' => false));
68
69
        if (PEAR::isError($db)) {
70
            echo "ERROR: " . $db->getMessage() . "\n";
71
            return NULL;
72
        }
73
74
        $conn = $db->connection;
75
76
        $succ = mysqli_set_charset($conn, $charset);
77
78
        if (!$succ) {
79
            echo "ERROR: couldn't set charset\n";
80
            $db->disconnect();
81
            return NULL;
82
        }
83
84
        $result = $db->autoCommit(true);
85
86
        if (PEAR::isError($result)) {
87
            echo "ERROR: " . $result->getMessage() . "\n";
88
            $db->disconnect();
89
            return NULL;
90
        }
91
92
        return $db;
93
    }
94
95
    function fixup()
96
    {
97
        $this->fixupNotices($this->args['max_notice'],
98
                            $this->args['min_notice']);
99
        $this->fixupProfiles();
100
        $this->fixupGroups();
101
        $this->fixupMessages();
102
    }
103
104
    function fixupNotices($max_id, $min_id) {
105
106
        // Do a separate DB connection
107
108
        $sth = $this->dbu->prepare("UPDATE notice SET content = UNHEX(?), rendered = UNHEX(?) WHERE id = ?");
109
110
        if (PEAR::isError($sth)) {
111
            echo "ERROR: " . $sth->getMessage() . "\n";
112
            return;
113
        }
114
115
        $sql = 'SELECT id, content, rendered FROM notice ' .
116
          'WHERE LENGTH(content) != CHAR_LENGTH(content) '.
117
          'AND modified < "'.$this->max_date.'" ';
118
119
        if (!empty($max_id)) {
120
            $sql .= ' AND id <= ' . $max_id;
121
        }
122
123
        if (!empty($min_id)) {
124
            $sql .= ' AND id >= ' . $min_id;
125
        }
126
127
        $sql .= ' ORDER BY id DESC';
128
129
        $rn = $this->dbl->query($sql);
130
131
        if (PEAR::isError($rn)) {
132
            echo "ERROR: " . $rn->getMessage() . "\n";
133
            return;
134
        }
135
136
        echo "Number of rows: " . $rn->numRows() . "\n";
137
138
        $notice = array();
139
140
        while (DB_OK == $rn->fetchInto($notice)) {
141
142
            $id = ($notice[0])+0;
143
            $content = bin2hex($notice[1]);
144
            $rendered = bin2hex($notice[2]);
145
146
            echo "$id...";
147
148
            $result = $this->dbu->execute($sth, array($content, $rendered, $id));
149
150
            if (PEAR::isError($result)) {
151
                echo "ERROR: " . $result->getMessage() . "\n";
152
                continue;
153
            }
154
155
            $cnt = $this->dbu->affectedRows();
156
157
            if ($cnt != 1) {
158
                echo "ERROR: 0 rows affected\n";
159
                continue;
160
            }
161
162
            $notice = Notice::staticGet('id', $id);
163
            $notice->decache();
164
            $notice->free();
165
166
            echo "OK\n";
167
        }
168
    }
169
170
    function fixupProfiles()
171
    {
172
        // Do a separate DB connection
173
174
        $sth = $this->dbu->prepare("UPDATE profile SET ".
175
                                   "fullname = UNHEX(?),".
176
                                   "location = UNHEX(?), ".
177
                                   "bio = UNHEX(?) ".
178
                                   "WHERE id = ?");
179
180
        if (PEAR::isError($sth)) {
181
            echo "ERROR: " . $sth->getMessage() . "\n";
182
            return;
183
        }
184
185
        $sql = 'SELECT id, fullname, location, bio FROM profile ' .
186
          'WHERE (LENGTH(fullname) != CHAR_LENGTH(fullname) '.
187
          'OR LENGTH(location) != CHAR_LENGTH(location) '.
188
          'OR LENGTH(bio) != CHAR_LENGTH(bio)) '.
189
          'AND modified < "'.$this->max_date.'" '.
190
          ' ORDER BY modified DESC';
191
192
        $rn = $this->dbl->query($sql);
193
194
        if (PEAR::isError($rn)) {
195
            echo "ERROR: " . $rn->getMessage() . "\n";
196
            return;
197
        }
198
199
        echo "Number of rows: " . $rn->numRows() . "\n";
200
201
        $profile = array();
202
203
        while (DB_OK == $rn->fetchInto($profile)) {
204
205
            $id = ($profile[0])+0;
206
            $fullname = bin2hex($profile[1]);
207
            $location = bin2hex($profile[2]);
208
            $bio = bin2hex($profile[3]);
209
210
            echo "$id...";
211
212
            $result = $this->dbu->execute($sth, array($fullname, $location, $bio, $id));
213
214
            if (PEAR::isError($result)) {
215
                echo "ERROR: " . $result->getMessage() . "\n";
216
                continue;
217
            }
218
219
            $cnt = $this->dbu->affectedRows();
220
221
            if ($cnt != 1) {
222
                echo "ERROR: 0 rows affected\n";
223
                continue;
224
            }
225
226
            $profile = Profile::staticGet('id', $id);
227
            $profile->decache();
228
            $profile->free();
229
230
            echo "OK\n";
231
        }
232
    }
233
234
    function fixupGroups()
235
    {
236
        // Do a separate DB connection
237
238
        $sth = $this->dbu->prepare("UPDATE user_group SET ".
239
                                   "fullname = UNHEX(?),".
240
                                   "location = UNHEX(?), ".
241
                                   "description = UNHEX(?) ".
242
                                   "WHERE id = ?");
243
244
        if (PEAR::isError($sth)) {
245
            echo "ERROR: " . $sth->getMessage() . "\n";
246
            return;
247
        }
248
249
        $sql = 'SELECT id, fullname, location, description FROM user_group ' .
250
          'WHERE LENGTH(fullname) != CHAR_LENGTH(fullname) '.
251
          'OR LENGTH(location) != CHAR_LENGTH(location) '.
252
          'OR LENGTH(description) != CHAR_LENGTH(description) '.
253
          'AND modified < "'.$this->max_date.'" '.
254
          'ORDER BY modified DESC';
255
256
        $rn = $this->dbl->query($sql);
257
258
        if (PEAR::isError($rn)) {
259
            echo "ERROR: " . $rn->getMessage() . "\n";
260
            return;
261
        }
262
263
        echo "Number of rows: " . $rn->numRows() . "\n";
264
265
        $user_group = array();
266
267
        while (DB_OK == $rn->fetchInto($user_group)) {
268
269
            $id = ($user_group[0])+0;
270
            $fullname = bin2hex($user_group[1]);
271
            $location = bin2hex($user_group[2]);
272
            $description = bin2hex($user_group[3]);
273
274
            echo "$id...";
275
276
            $result = $this->dbu->execute($sth, array($fullname, $location, $description, $id));
277
278
            if (PEAR::isError($result)) {
279
                echo "ERROR: " . $result->getMessage() . "\n";
280
                continue;
281
            }
282
283
            $cnt = $this->dbu->affectedRows();
284
285
            if ($cnt != 1) {
286
                echo "ERROR: 0 rows affected\n";
287
                continue;
288
            }
289
290
            $user_group = User_group::staticGet('id', $id);
291
            $user_group->decache();
292
            $user_group->free();
293
294
            echo "OK\n";
295
        }
296
    }
297
298
    function fixupMessages() {
299
300
        // Do a separate DB connection
301
302
        $sth = $this->dbu->prepare("UPDATE message SET content = UNHEX(?), rendered = UNHEX(?) WHERE id = ?");
303
304
        if (PEAR::isError($sth)) {
305
            echo "ERROR: " . $sth->getMessage() . "\n";
306
            return;
307
        }
308
309
        $sql = 'SELECT id, content, rendered FROM message ' .
310
          'WHERE LENGTH(content) != CHAR_LENGTH(content) '.
311
          'AND modified < "'.$this->max_date.'" '.
312
          'ORDER BY id DESC';
313
314
        $rn = $this->dbl->query($sql);
315
316
        if (PEAR::isError($rn)) {
317
            echo "ERROR: " . $rn->getMessage() . "\n";
318
            return;
319
        }
320
321
        echo "Number of rows: " . $rn->numRows() . "\n";
322
323
        $message = array();
324
325
        while (DB_OK == $rn->fetchInto($message)) {
326
327
            $id = ($message[0])+0;
328
            $content = bin2hex($message[1]);
329
            $rendered = bin2hex($message[2]);
330
331
            echo "$id...";
332
333
            $result = $this->dbu->execute($sth, array($content, $rendered, $id));
334
335
            if (PEAR::isError($result)) {
336
                echo "ERROR: " . $result->getMessage() . "\n";
337
                continue;
338
            }
339
340
            $cnt = $this->dbu->affectedRows();
341
342
            if ($cnt != 1) {
343
                echo "ERROR: 0 rows affected\n";
344
                continue;
345
            }
346
347
            $message = Message::staticGet('id', $id);
348
            $message->decache();
349
            $message->free();
350
351
            echo "OK\n";
352
        }
353
    }
354
}
355
356
$max_date = (count($args) > 0) ? $args[0] : null;
357
$max_id = (count($args) > 1) ? $args[1] : null;
358
$min_id = (count($args) > 2) ? $args[2] : null;
359
360
$fixer = new UTF8FixerUpper(array('max_date' => $max_date,
361
                                  'max_notice' => $max_id,
362
                                  'min_notice' => $min_id));
363
364
$fixer->fixup();