4 * Routines for Mime mailification of pages.
8 * Routines for quoted-printable en/decoding.
10 function QuotedPrintableEncode($string)
12 // Quote special characters in line.
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]));
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',
30 function QuotedPrintableDecode($string)
32 // Eliminate soft line-breaks.
33 $string = preg_replace('/=[ \t\r]*\n/', '', $string);
34 return quoted_printable_decode($string);
37 define('MIME_TOKEN_REGEXP', "[-!#-'*+.0-9A-Z^-~]+");
39 function MimeContentTypeHeader($type, $subtype, $params)
41 $header = "Content-Type: $type/$subtype";
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";
52 function MimeMultipart($parts)
54 global $mime_multipart_count;
56 // The string "=_" can not occur in quoted-printable encoded data.
57 $boundary = "=_multipart_boundary_" . ++$mime_multipart_count;
59 $head = MimeContentTypeHeader('multipart', 'mixed',
60 array('boundary' => $boundary));
62 $sep = "\r\n--$boundary\r\n";
64 return $head . $sep . implode($sep, $parts) . "\r\n--${boundary}--\r\n";
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).
73 * Also see http://www.faqs.org/rfcs/rfc2822.html
76 * Notes on content-transfer-encoding.
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.
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.
90 * @param WikiDB_Page $page
91 * @param WikiDB_PageRevision $revision
95 function MimeifyPageRevision(&$page, &$revision)
97 // $wikidb =& $revision->_wikidb;
98 // $page = $wikidb->getPage($revision->getName());
99 // FIXME: add 'hits' to $params
100 $params = array('pagename' => $page->getName(),
102 'author' => $revision->get('author'),
103 'owner' => $page->getOwner(),
104 'version' => $revision->getVersion(),
105 'lastmodified' => $revision->get('mtime'));
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,...
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']);
136 $out = MimeContentTypeHeader('application', 'x-phpwiki', $params);
137 $out .= sprintf("Content-Transfer-Encoding: %s\r\n",
138 STRICT_MAILABLE_PAGEDUMPS ? 'quoted-printable' : 'binary');
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));
153 * Routines for parsing Mime-ified phpwiki pages.
155 function ParseRFC822Headers(&$string)
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]));
162 while (preg_match('/^([!-9;-~]+) [ \t]* : [ \t]* '
163 . '( .* \r?\n (?: [ \t] .* \r?\n)* )/x',
165 $headers[strtolower($match[1])]
166 = preg_replace('/^\s+|\s+$/', '', $match[2]);
167 $string = substr($string, strlen($match[0]));
173 if (strlen($string) > 0) {
174 if (!preg_match("/^\r?\n/", $string, $match)) {
175 // No blank line after headers.
178 $string = substr($string, strlen($match[0]));
184 function ParseMimeContentType($string)
186 // FIXME: Remove (RFC822 style comments).
189 if (!preg_match(':^\s*(' . MIME_TOKEN_REGEXP . ')\s*'
191 . '\s*(' . MIME_TOKEN_REGEXP . ')\s*:x',
194 ExitWiki(sprintf("Bad %s", 'MIME content-type'));
196 $type = strtolower($match[1]);
197 $subtype = strtolower($match[2]);
198 $string = substr($string, strlen($match[0]));
201 while (preg_match('/^;\s*(' . MIME_TOKEN_REGEXP . ')\s*=\s*'
202 . '(?:(' . MIME_TOKEN_REGEXP . ')|"((?:[^"\\\\]|\\.)*)") \s*/sx',
204 //" <--kludge for brain-dead syntax coloring
205 if (strlen($match[2]))
208 $val = preg_replace('/[\\\\](.)/s', '\\1', $match[3]);
210 $param[strtolower($match[1])] = $val;
212 $string = substr($string, strlen($match[0]));
215 return array($type, $subtype, $param);
218 function ParseMimeMultipart($data, $boundary)
221 ExitWiki("No boundary?");
224 $boundary = preg_quote($boundary);
226 while (preg_match("/^(|.*?\n)--$boundary((?:--)?)[^\n]*\n/s",
228 $data = substr($data, strlen($match[0]));
229 if (!isset($parts)) {
230 $parts = array(); // First time through: discard leading chaff
232 if ($content = ParseMimeifiedPages($match[1]))
233 for (reset($content); $p = current($content); next($content))
238 return $parts; // End boundary found.
240 ExitWiki("No end boundary?");
243 function GenerateFootnotesFromRefs($params)
245 $footnotes = array();
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));
253 if (sizeof($footnotes) > 0) {
256 . "!" . _("References") . "\n"
257 . join("\n%%%\n", $footnotes) . "\n";
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)
267 if (!class_exists('PagePermission')) {
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;
283 $perm = new PagePermission($hash);
285 return serialize($perm->perm);
288 // Convert references in meta-data to footnotes.
289 // Only zip archives generated by phpwiki 1.2.x or earlier should have
291 function ParseMimeifiedPages($data)
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);
297 if (!($headers = ParseRFC822Headers($data))
298 || empty($headers['content-type'])
300 //trigger_error( sprintf(_("Can't find %s"),'content-type header'),
304 $typeheader = $headers['content-type'];
306 if (!(list ($type, $subtype, $params) = ParseMimeContentType($typeheader))) {
307 trigger_error(sprintf("Can't parse %s: (%s)",
308 'content-type', $typeheader),
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"),
320 // FIXME: more sanity checking?
323 $versiondata = array();
324 if (isset($headers['date']))
325 $pagedata['date'] = strtotime($headers['date']);
327 //DONE: support owner and acl
328 foreach ($params as $key => $value) {
331 $value = rawurldecode($value);
335 $page[$key] = $value;
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';
346 $pagedata[$key] = $value;
350 if (class_exists('PagePermission')) {
351 $pagedata['perm'] = ParseMimeifiedPerm($value);
355 $versiondata['mtime'] = $value;
361 $versiondata[$key] = $value;
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'])) {
370 if (is_object($request)) {
371 $user = $request->getUser();
372 $versiondata['author'] = $user->getId(); //FIXME:?
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'));
382 $data .= GenerateFootnotesFromRefs($params);
384 $page['content'] = preg_replace('/[ \t\r]*\n/', "\n", chop($data));
385 $page['pagedata'] = $pagedata;
386 $page['versiondata'] = $versiondata;
395 // c-hanging-comment-ender-p: nil
396 // indent-tabs-mode: nil