]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/mimelib.php
Update translations
[SourceForge/phpwiki.git] / lib / mimelib.php
1 <?php
2
3 /**
4  * Routines for Mime mailification of pages.
5  */
6
7 /**
8  * Routines for quoted-printable en/decoding.
9  */
10 function QuotedPrintableEncode($string)
11 {
12     // Quote special characters in line.
13     $quoted = "";
14     while ($string) {
15         // The complicated regexp is to force quoting of trailing spaces.
16         preg_match('/^([ !-<>-~]*)(?:([!-<>-~]$)|(.))/s', $string, $match);
17         $quoted .= $match[1] . $match[2];
18         if (!empty($match[3]))
19             $quoted .= sprintf("=%02X", ord($match[3]));
20         $string = substr($string, strlen($match[0]));
21     }
22     // Split line.
23     // This splits the line (preferably after white-space) into lines
24     // which are no longer than 76 chars (after adding trailing '=' for
25     // soft line break, but before adding \r\n.)
26     return preg_replace('/(?=.{77})(.{10,74}[ \t]|.{71,73}[^=][^=])/s',
27         "\\1=\r\n", $quoted);
28 }
29
30 function QuotedPrintableDecode($string)
31 {
32     // Eliminate soft line-breaks.
33     $string = preg_replace('/=[ \t\r]*\n/', '', $string);
34     return quoted_printable_decode($string);
35 }
36
37 define('MIME_TOKEN_REGEXP', "[-!#-'*+.0-9A-Z^-~]+");
38
39 function MimeContentTypeHeader($type, $subtype, $params)
40 {
41     $header = "Content-Type: $type/$subtype";
42     reset($params);
43     while (list($key, $val) = each($params)) {
44         //FIXME:  what about non-ascii printables in $val?
45         if (!preg_match('/^' . MIME_TOKEN_REGEXP . '$/', $val))
46             $val = '"' . addslashes($val) . '"';
47         $header .= ";\r\n  $key=$val";
48     }
49     return "$header\r\n";
50 }
51
52 function MimeMultipart($parts)
53 {
54     global $mime_multipart_count;
55
56     // The string "=_" can not occur in quoted-printable encoded data.
57     $boundary = "=_multipart_boundary_" . ++$mime_multipart_count;
58
59     $head = MimeContentTypeHeader('multipart', 'mixed',
60         array('boundary' => $boundary));
61
62     $sep = "\r\n--$boundary\r\n";
63
64     return $head . $sep . implode($sep, $parts) . "\r\n--${boundary}--\r\n";
65 }
66
67 /**
68  * For reference see:
69  * http://www.nacs.uci.edu/indiv/ehood/MIME/2045/rfc2045.html
70  * http://www.faqs.org/rfcs/rfc2045.html
71  * (RFC 1521 has been superceeded by RFC 2045 & others).
72  *
73  * Also see http://www.faqs.org/rfcs/rfc2822.html
74  *
75  *
76  * Notes on content-transfer-encoding.
77  *
78  * "7bit" means short lines of US-ASCII.
79  * "8bit" means short lines of octets with (possibly) the high-order bit set.
80  * "binary" means lines are not necessarily short enough for SMTP
81  * transport, and non-ASCII characters may be present.
82  *
83  * Only "7bit", "quoted-printable", and "base64" are universally safe
84  * for transport via e-mail.  (Though many MTAs can/will be configured to
85  * automatically convert encodings to a safe type if they receive
86  * mail encoded in '8bit' and/or 'binary' encodings.
87  */
88
89 /**
90  * @param WikiDB_Page $page
91  * @param WikiDB_PageRevision $revision
92  * @return string
93  */
94
95 function MimeifyPageRevision(&$page, &$revision)
96 {
97     // $wikidb =& $revision->_wikidb;
98     // $page = $wikidb->getPage($revision->getName());
99     // FIXME: add 'hits' to $params
100     $params = array('pagename' => $page->getName(),
101         'flags' => "",
102         'author' => $revision->get('author'),
103         'owner' => $page->getOwner(),
104         'version' => $revision->getVersion(),
105         'lastmodified' => $revision->get('mtime'));
106
107     if ($page->get('mtime'))
108         $params['created'] = $page->get('mtime');
109     if ($page->get('locked'))
110         $params['flags'] = 'PAGE_LOCKED';
111     if (ENABLE_EXTERNAL_PAGES && $page->get('external'))
112         $params['flags'] = ($params['flags'] ? $params['flags'] . ',EXTERNAL_PAGE' : 'EXTERNAL_PAGE');
113     if ($revision->get('author_id'))
114         $params['author_id'] = $revision->get('author_id');
115     if ($revision->get('summary'))
116         $params['summary'] = $revision->get('summary');
117     if ($page->get('hits'))
118         $params['hits'] = $page->get('hits');
119     if ($page->get('owner'))
120         $params['owner'] = $page->get('owner');
121     if ($page->get('perm') and class_exists('PagePermission')) {
122         $acl = getPagePermissions($page);
123         $params['acl'] = $acl->asAclLines();
124         //TODO: convert to multiple lines? acl-view => groups,...; acl-edit => groups,...
125     }
126
127     // Non-US-ASCII is not allowed in Mime headers (at least not without
128     // special handling) --- so we urlencode all parameter values.
129     foreach ($params as $key => $val)
130         $params[$key] = rawurlencode($val);
131     if (isset($params['acl']))
132         // default: "view:_EVERY; edit:_AUTHENTICATED; create:_AUTHENTICATED,_BOGOUSER; ".
133         //          "list:_EVERY; remove:_ADMIN,_OWNER; change:_ADMIN,_OWNER; dump:_EVERY; "
134         $params['acl'] = str_replace(array("%3A", "%3B%20", "%2C"), array(":", "; ", ","), $params['acl']);
135
136     $out = MimeContentTypeHeader('application', 'x-phpwiki', $params);
137     $out .= sprintf("Content-Transfer-Encoding: %s\r\n",
138         STRICT_MAILABLE_PAGEDUMPS ? 'quoted-printable' : 'binary');
139
140     $out .= "\r\n";
141
142     foreach ($revision->getContent() as $line) {
143         // This is a dirty hack to allow saving binary text files. See above.
144         $line = rtrim($line);
145         if (STRICT_MAILABLE_PAGEDUMPS)
146             $line = QuotedPrintableEncode(rtrim($line));
147         $out .= "$line\r\n";
148     }
149     return $out;
150 }
151
152 /**
153  * Routines for parsing Mime-ified phpwiki pages.
154  */
155 function ParseRFC822Headers(&$string)
156 {
157     if (preg_match("/^From (.*)\r?\n/", $string, $match)) {
158         $headers['from '] = preg_replace('/^\s+|\s+$/', '', $match[1]);
159         $string = substr($string, strlen($match[0]));
160     }
161
162     while (preg_match('/^([!-9;-~]+) [ \t]* : [ \t]* '
163             . '( .* \r?\n (?: [ \t] .* \r?\n)* )/x',
164         $string, $match)) {
165         $headers[strtolower($match[1])]
166             = preg_replace('/^\s+|\s+$/', '', $match[2]);
167         $string = substr($string, strlen($match[0]));
168     }
169
170     if (empty($headers))
171         return false;
172
173     if (strlen($string) > 0) {
174         if (!preg_match("/^\r?\n/", $string, $match)) {
175             // No blank line after headers.
176             return false;
177         }
178         $string = substr($string, strlen($match[0]));
179     }
180
181     return $headers;
182 }
183
184 function ParseMimeContentType($string)
185 {
186     // FIXME: Remove (RFC822 style comments).
187
188     // Get type/subtype
189     if (!preg_match(':^\s*(' . MIME_TOKEN_REGEXP . ')\s*'
190             . '/'
191             . '\s*(' . MIME_TOKEN_REGEXP . ')\s*:x',
192         $string, $match)
193     )
194         ExitWiki(sprintf("Bad %s", 'MIME content-type'));
195
196     $type = strtolower($match[1]);
197     $subtype = strtolower($match[2]);
198     $string = substr($string, strlen($match[0]));
199
200     $param = array();
201     while (preg_match('/^;\s*(' . MIME_TOKEN_REGEXP . ')\s*=\s*'
202             . '(?:(' . MIME_TOKEN_REGEXP . ')|"((?:[^"\\\\]|\\.)*)") \s*/sx',
203         $string, $match)) {
204         //" <--kludge for brain-dead syntax coloring
205         if (strlen($match[2]))
206             $val = $match[2];
207         else
208             $val = preg_replace('/[\\\\](.)/s', '\\1', $match[3]);
209
210         $param[strtolower($match[1])] = $val;
211
212         $string = substr($string, strlen($match[0]));
213     }
214
215     return array($type, $subtype, $param);
216 }
217
218 function ParseMimeMultipart($data, $boundary)
219 {
220     if (!$boundary) {
221         ExitWiki("No boundary?");
222     }
223
224     $boundary = preg_quote($boundary);
225
226     while (preg_match("/^(|.*?\n)--$boundary((?:--)?)[^\n]*\n/s",
227         $data, $match)) {
228         $data = substr($data, strlen($match[0]));
229         if (!isset($parts)) {
230             $parts = array(); // First time through: discard leading chaff
231         } else {
232             if ($content = ParseMimeifiedPages($match[1]))
233                 for (reset($content); $p = current($content); next($content))
234                     $parts[] = $p;
235         }
236
237         if ($match[2])
238             return $parts; // End boundary found.
239     }
240     ExitWiki("No end boundary?");
241 }
242
243 function GenerateFootnotesFromRefs($params)
244 {
245     $footnotes = array();
246     reset($params);
247     while (list($p, $reference) = each($params)) {
248         if (preg_match('/^ref([1-9][0-9]*)$/', $p, $m))
249             $footnotes[$m[1]] = sprintf(_("[%d] See [%s]"),
250                 $m[1], rawurldecode($reference));
251     }
252
253     if (sizeof($footnotes) > 0) {
254         ksort($footnotes);
255         return "-----\n"
256             . "!" . _("References") . "\n"
257             . join("\n%%%\n", $footnotes) . "\n";
258     } else
259         return "";
260 }
261
262 // counterpart to $acl->asAclLines() and rawurl undecode
263 // default: "view:_EVERY; edit:_AUTHENTICATED; create:_AUTHENTICATED,_BOGOUSER; ".
264 //          "list:_EVERY; remove:_ADMIN,_OWNER; change:_ADMIN,_OWNER; dump:_EVERY; "
265 function ParseMimeifiedPerm($string)
266 {
267     if (!class_exists('PagePermission')) {
268         return '';
269     }
270     $hash = array();
271     foreach (explode(";", trim($string)) as $accessgroup) {
272         list($access, $groupstring) = explode(":", trim($accessgroup));
273         $access = trim($access);
274         $groups = explode(",", trim($groupstring));
275         foreach ($groups as $group) {
276             $group = trim($group);
277             $bool = (boolean)(substr($group, 0, 1) != '-');
278             if (substr($group, 0, 1) == '-' or substr($group, 0, 1) == '+')
279                 $group = substr($group, 1);
280             $hash[$access][$group] = $bool;
281         }
282     }
283     $perm = new PagePermission($hash);
284     $perm->sanify();
285     return serialize($perm->perm);
286 }
287
288 // Convert references in meta-data to footnotes.
289 // Only zip archives generated by phpwiki 1.2.x or earlier should have
290 // references.
291 function ParseMimeifiedPages($data)
292 {
293     // We may need a lot of memory and time for the dump
294     ini_set("memory_limit", -1);
295     ini_set('max_execution_time', 0);
296
297     if (!($headers = ParseRFC822Headers($data))
298         || empty($headers['content-type'])
299     ) {
300         //trigger_error( sprintf(_("Can't find %s"),'content-type header'),
301         //               E_USER_WARNING );
302         return false;
303     }
304     $typeheader = $headers['content-type'];
305
306     if (!(list ($type, $subtype, $params) = ParseMimeContentType($typeheader))) {
307         trigger_error(sprintf("Can't parse %s: (%s)",
308                 'content-type', $typeheader),
309             E_USER_WARNING);
310         return false;
311     }
312     if ("$type/$subtype" == 'multipart/mixed') {
313         return ParseMimeMultipart($data, $params['boundary']);
314     } elseif ("$type/$subtype" != 'application/x-phpwiki') {
315         trigger_error(sprintf("Bad %s", "content-type: $type/$subtype"),
316             E_USER_WARNING);
317         return false;
318     }
319
320     // FIXME: more sanity checking?
321     $page = array();
322     $pagedata = array();
323     $versiondata = array();
324     if (isset($headers['date']))
325         $pagedata['date'] = strtotime($headers['date']);
326
327     //DONE: support owner and acl
328     foreach ($params as $key => $value) {
329         if (empty($value))
330             continue;
331         $value = rawurldecode($value);
332         switch ($key) {
333             case 'pagename':
334             case 'version':
335                 $page[$key] = $value;
336                 break;
337             case 'flags':
338                 if (preg_match('/PAGE_LOCKED/', $value))
339                     $pagedata['locked'] = 'yes';
340                 if (ENABLE_EXTERNAL_PAGES && preg_match('/EXTERNAL_PAGE/', $value))
341                     $pagedata['external'] = 'yes';
342                 break;
343             case 'owner':
344             case 'created':
345             case 'hits':
346                 $pagedata[$key] = $value;
347                 break;
348             case 'acl':
349             case 'perm':
350                 if (class_exists('PagePermission')) {
351                     $pagedata['perm'] = ParseMimeifiedPerm($value);
352                 }
353                 break;
354             case 'lastmodified':
355                 $versiondata['mtime'] = $value;
356                 break;
357             case 'author':
358             case 'author_id':
359             case 'summary':
360             case 'pagetype':
361                 $versiondata[$key] = $value;
362                 break;
363         }
364     }
365
366     // FIXME: do we need to try harder to find a pagename if we
367     //        haven't got one yet?
368     if (!isset($versiondata['author'])) {
369         global $request;
370         if (is_object($request)) {
371             $user = $request->getUser();
372             $versiondata['author'] = $user->getId(); //FIXME:?
373         }
374     }
375
376     $encoding = strtolower($headers['content-transfer-encoding']);
377     if ($encoding == 'quoted-printable')
378         $data = QuotedPrintableDecode($data);
379     else if ($encoding && $encoding != 'binary')
380         ExitWiki(sprintf("Unknown %s", 'encoding type: $encoding'));
381
382     $data .= GenerateFootnotesFromRefs($params);
383
384     $page['content'] = preg_replace('/[ \t\r]*\n/', "\n", chop($data));
385     $page['pagedata'] = $pagedata;
386     $page['versiondata'] = $versiondata;
387
388     return array($page);
389 }
390
391 // Local Variables:
392 // mode: php
393 // tab-width: 8
394 // c-basic-offset: 4
395 // c-hanging-comment-ender-p: nil
396 // indent-tabs-mode: nil
397 // End: