]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiPluginCached.php
Reformat code
[SourceForge/phpwiki.git] / lib / WikiPluginCached.php
1 <?php
2 /*
3  * Copyright (C) 2002 Johannes Große
4  * Copyright (C) 2004,2007 Reini Urban
5  *
6  * This file is part of PhpWiki.
7  *
8  * PhpWiki is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * PhpWiki is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21  */
22
23 /**
24  * You should set up the options in config/config.ini at Part seven:
25  * $ pear install http://pear.php.net/get/Cache
26  * This file belongs to WikiPluginCached.
27  */
28
29 require_once 'lib/WikiPlugin.php';
30 // require_once "lib/plugincache-config.php"; // replaced by config.ini settings!
31
32 // types:
33 define('PLUGIN_CACHED_HTML', 0); // cached html (extensive calculation)
34 define('PLUGIN_CACHED_IMG_INLINE', 1); // gd images
35 define('PLUGIN_CACHED_MAP', 2); // area maps
36 define('PLUGIN_CACHED_SVG', 3); // special SVG/SVGZ object
37 define('PLUGIN_CACHED_SVG_PNG', 4); // special SVG/SVGZ object with PNG fallback
38 define('PLUGIN_CACHED_SWF', 5); // special SWF (flash) object
39 define('PLUGIN_CACHED_PDF', 6); // special PDF object (inlinable?)
40 define('PLUGIN_CACHED_PS', 7); // special PS object (inlinable?)
41 // boolean tests:
42 define('PLUGIN_CACHED_IMG_ONDEMAND', 64); // don't cache
43 define('PLUGIN_CACHED_STATIC', 128); // make it available via /uploads/, not via /getimg.php?id=
44
45 /**
46  * An extension of the WikiPlugin class to allow image output and
47  * cacheing.
48  * There are several abstract functions to be overloaded.
49  * Have a look at the example files
50  * <ul><li>plugin/TexToPng.php</li>
51  *     <li>plugin/CacheTest.php (extremely simple example)</li>
52  *     <li>plugin/RecentChangesCached.php</li>
53  *     <li>plugin/VisualWiki.php</li>
54  *     <li>plugin/Ploticus.php</li>
55  * </ul>
56  *
57  * @author  Johannes Große, Reini Urban
58  */
59 class WikiPluginCached extends WikiPlugin
60 {
61     var $_static;
62
63     /**
64      * Produces URL and id number from plugin arguments which later on,
65      * will allow to find a cached image or to reconstruct the complete
66      * plugin call to recreate the image.
67      *
68      * @param cache    object the cache object used to store the images
69      * @param argarray array  all parameters (including those set to
70      *                        default values) of the plugin call to be
71      *                        prepared
72      * @access private
73      * @return array(id,url)
74      *
75      * TODO: check if args is needed at all (on lost cache)
76      */
77     function genUrl($cache, $argarray)
78     {
79         global $request;
80         //$cacheparams = $GLOBALS['CacheParams'];
81
82         $plugincall = serialize(array(
83             'pluginname' => $this->getName(),
84             'arguments' => $argarray));
85         $id = $cache->generateId($plugincall);
86         $plugincall_arg = rawurlencode($plugincall);
87         //$plugincall_arg = md5($plugincall);
88         // will not work if plugin has to recreate content and cache is lost
89
90         $url = DATA_PATH . '/getimg.php?';
91         if (($lastchar = substr($url, -1)) == '/') {
92             $url = substr($url, 0, -1);
93         }
94         if (strlen($plugincall_arg) > PLUGIN_CACHED_MAXARGLEN) {
95             // we can't send the data as URL so we just send the id
96             if (!$request->getSessionVar('imagecache' . $id)) {
97                 $request->setSessionVar('imagecache' . $id, $plugincall);
98             }
99             $plugincall_arg = false; // not needed anymore
100         }
101
102         if ($lastchar == '?') {
103             // this indicates that a direct call of the image creation
104             // script is wished ($url is assumed to link to the script)
105             $url .= "id=$id" . ($plugincall_arg ? '&args=' . $plugincall_arg : '');
106         } else {
107             // Not yet supported.
108             // We are supposed to use the indirect 404 ErrorDocument method
109             // ($url is assumed to be the url of the image in
110             //  cache_dir and the image creation script is referred to in the
111             //  ErrorDocument 404 directive.)
112             $url .= '/' . PLUGIN_CACHED_FILENAME_PREFIX . $id . '.img'
113                 . ($plugincall_arg ? '?args=' . $plugincall_arg : '');
114         }
115         if ($request->getArg("start_debug") and (DEBUG & _DEBUG_REMOTE))
116             $url .= "&start_debug=1";
117         return array($id, $url);
118     } // genUrl
119
120     /**
121      * Replaces the abstract run method of WikiPlugin to implement
122      * a cache check which can avoid redundant runs.
123      * <b>Do not override this method in a subclass. Instead you may
124      * rename your run method to getHtml, getImage or getMap.
125      * Have a close look on the arguments and required return values,
126      * however. </b>
127      *
128      * @access protected
129      * @param  dbi       WikiDB  database abstraction class
130      * @param  argstr    string  plugin arguments in the call from PhpWiki
131      * @param  request   Request ???
132      * @param  string    basepage Pagename to use to interpret links [/relative] page names.
133      * @return string HTML output to be printed to browser
134      *
135      * @see #getHtml
136      * @see #getImage
137      * @see #getMap
138      */
139     function run($dbi, $argstr, &$request, $basepage)
140     {
141         $cache = $this->newCache();
142         //$cacheparams = $GLOBALS['CacheParams'];
143
144         $sortedargs = $this->getArgs($argstr, $request);
145         if (is_array($sortedargs))
146             ksort($sortedargs);
147         $this->_args =& $sortedargs;
148         $this->_type = $this->getPluginType();
149         $this->_static = false;
150         if ($this->_type & PLUGIN_CACHED_STATIC
151             or $request->getArg('action') == 'pdf'
152         ) // htmldoc doesn't grok subrequests
153         {
154             $this->_type = $this->_type & ~PLUGIN_CACHED_STATIC;
155             $this->_static = true;
156         }
157
158         // ---------- embed static image, no getimg.php? url -----------------
159         if (0 and $this->_static) {
160             //$content = $cache->get($id, 'imagecache');
161             $content = array();
162             if ($this->produceImage($content, $this, $dbi, $sortedargs, $request, 'html')) {
163                 // save the image in uploads
164                 return $this->embedImg($content['url'], $dbi, $sortedargs, $request);
165             } else {
166                 // copy the cached image into uploads if older
167                 return HTML();
168             }
169         }
170
171         list($id, $url) = $this->genUrl($cache, $sortedargs);
172         // ---------- don't check cache: html and img gen. -----------------
173         // override global PLUGIN_CACHED_USECACHE for a plugin
174         if ($this->getPluginType() & PLUGIN_CACHED_IMG_ONDEMAND) {
175             if ($this->_static and $this->produceImage($content, $this, $dbi, $sortedargs, $request, 'html'))
176                 $url = $content['url'];
177             return $this->embedImg($url, $dbi, $sortedargs, $request);
178         }
179
180         $do_save = false;
181         $content = $cache->get($id, 'imagecache');
182         switch ($this->_type) {
183             case PLUGIN_CACHED_HTML:
184                 if (!$content || !$content['html']) {
185                     $this->resetError();
186                     $content['html'] = $this->getHtml($dbi, $sortedargs, $request, $basepage);
187                     if ($errortext = $this->getError()) {
188                         $this->printError($errortext, 'html');
189                         return HTML();
190                     }
191                     $do_save = true;
192                 }
193                 break;
194             case PLUGIN_CACHED_IMG_INLINE:
195                 if (PLUGIN_CACHED_USECACHE && (!$content || !$content['image'])) { // new
196                     $do_save = $this->produceImage($content, $this, $dbi, $sortedargs, $request, 'html');
197                     if ($this->_static) $url = $content['url'];
198                     $content['html'] = $do_save ? $this->embedImg($url, $dbi, $sortedargs, $request) : false;
199                 } elseif (!empty($content['url']) && $this->_static) { // already in cache
200                     $content['html'] = $this->embedImg($content['url'], $dbi, $sortedargs, $request);
201                 } elseif (!empty($content['image']) && $this->_static) { // copy from cache to upload
202                     $do_save = $this->produceImage($content, $this, $dbi, $sortedargs, $request, 'html');
203                     $url = $content['url'];
204                     $content['html'] = $do_save ? $this->embedImg($url, $dbi, $sortedargs, $request) : false;
205                 }
206                 break;
207             case PLUGIN_CACHED_MAP:
208                 if (!$content || !$content['image'] || !$content['html']) {
209                     $do_save = $this->produceImage($content, $this, $dbi, $sortedargs, $request, 'html');
210                     if ($this->_static) $url = $content['url'];
211                     $content['html'] = $do_save
212                         ? $this->embedMap($id, $url, $content['html'], $dbi, $sortedargs, $request)
213                         : false;
214                 }
215                 break;
216             case PLUGIN_CACHED_SVG:
217                 if (!$content || !$content['html']) {
218                     $do_save = $this->produceImage($content, $this, $dbi, $sortedargs, $request, 'html');
219                     if ($this->_static) $url = $content['url'];
220                     $args = array(); //width+height => object args
221                     if (!empty($sortedargs['width'])) $args['width'] = $sortedargs['width'];
222                     if (!empty($sortedargs['height'])) $args['height'] = $sortedargs['height'];
223                     $content['html'] = $do_save
224                         ? $this->embedObject($url, 'image/svg+xml', $args,
225                             HTML::embed(array_merge(
226                                 array('src' => $url, 'type' => 'image/svg+xml'),
227                                 $args)))
228                         : false;
229                 }
230                 break;
231             case PLUGIN_CACHED_SVG_PNG:
232                 if (!$content || !$content['html']) {
233                     $do_save_svg = $this->produceImage($content, $this, $dbi, $sortedargs, $request, 'html');
234                     if ($this->_static) $url = $content['url'];
235                     // hack alert! somehow we should know which argument will produce the secondary image (PNG)
236                     $args = $sortedargs;
237                     $args[$this->pngArg()] = $content['imagetype']; // default type: PNG or GIF
238                     $do_save = $this->produceImage($pngcontent, $this, $dbi, $args, $request, $content['imagetype']);
239                     $args = array(); //width+height => object args
240                     if (!empty($sortedargs['width'])) $args['width'] = $sortedargs['width'];
241                     if (!empty($sortedargs['height'])) $args['height'] = $sortedargs['height'];
242                     $content['html'] = $do_save_svg
243                         ? $this->embedObject($url, 'image/svg+xml', $args,
244                             $this->embedImg($pngcontent['url'], $dbi, $sortedargs, $request))
245                         : false;
246                 }
247                 break;
248         }
249         if ($do_save) {
250             $content['args'] = md5($this->_pi);
251             $expire = $this->getExpire($dbi, $content['args'], $request);
252             $cache->save($id, $content, $expire, 'imagecache');
253         }
254         if ($content['html'])
255             return $content['html'];
256         return HTML();
257     } // run
258
259     /* --------------------- virtual or abstract functions ----------- */
260
261     /**
262      * Sets the type of the plugin to html, image or map
263      * production
264      *
265      * @access protected
266      * @return int determines the plugin to produce either html,
267      *             an image or an image map; uses on of the
268      *             following predefined values
269      *             <ul>
270      *             <li>PLUGIN_CACHED_HTML</li>
271      *             <li>PLUGIN_CACHED_IMG_INLINE</li>
272      *             <li>PLUGIN_CACHED_IMG_ONDEMAND</li>
273      *             <li>PLUGIN_CACHED_MAP</li>
274      *             </ul>
275      */
276     function getPluginType()
277     {
278         return PLUGIN_CACHED_IMG_ONDEMAND;
279     }
280
281     /**
282      * Creates an image handle from the given user arguments.
283      * This method is only called if the return value of
284      * <code>getPluginType</code> is set to
285      * PLUGIN_CACHED_IMG_INLINE or PLUGIN_CACHED_IMG_ONDEMAND.
286      *
287      * @access protected pure virtual
288      * @param  dbi       WikiDB       database abstraction class
289      * @param  argarray  array        complete (!) arguments to produce
290      *                                image. It is not necessary to call
291      *                                WikiPlugin->getArgs anymore.
292      * @param  request   Request      ???
293      * @return imagehandle image handle if successful
294      *                                false if an error occured
295      */
296     function getImage($dbi, $argarray, $request)
297     {
298         trigger_error('WikiPluginCached::getImage: pure virtual function in file '
299             . __FILE__ . ' line ' . __LINE__, E_USER_ERROR);
300         return false;
301     }
302
303     /**
304      * Sets the life time of a cache entry in seconds.
305      * Expired entries are not used anymore.
306      * During a garbage collection each expired entry is
307      * removed. If removing all expired entries is not
308      * sufficient, the expire time is ignored and removing
309      * is determined by the last "touch" of the entry.
310      *
311      * @access protected virtual
312      * @param  dbi       WikiDB       database abstraction class
313      * @param  argarray  array        complete (!) arguments. It is
314      *                                not necessary to call
315      *                                WikiPlugin->getArgs anymore.
316      * @param  request   Request      ???
317      * @return string format: '+seconds'
318      *                                '0' never expires
319      */
320     function getExpire($dbi, $argarray, $request)
321     {
322         return '0'; // persist forever
323     }
324
325     /**
326      * Decides the image type of an image output.
327      * Always used unless plugin type is PLUGIN_CACHED_HTML.
328      *
329      * @access protected virtual
330      * @param  dbi       WikiDB       database abstraction class
331      * @param  argarray  array        complete (!) arguments. It is
332      *                                not necessary to call
333      *                                WikiPlugin->getArgs anymore.
334      * @param  request   Request      ???
335      * @return string 'png', 'jpeg' or 'gif'
336      */
337     function getImageType(&$dbi, $argarray, &$request)
338     {
339         if (in_array($argarray['imgtype'], preg_split('/\s*:\s*/', PLUGIN_CACHED_IMGTYPES)))
340             return $argarray['imgtype'];
341         else
342             return 'png';
343     }
344
345     /**
346      * Produces the alt text for an image.
347      * <code> &lt;img src=... alt="getAlt(...)"&gt; </code>
348      *
349      * @access protected virtual
350      * @param  dbi       WikiDB       database abstraction class
351      * @param  argarray  array        complete (!) arguments. It is
352      *                                not necessary to call
353      *                                WikiPlugin->getArgs anymore.
354      * @param  request   Request      ???
355      * @return string "alt" description of the image
356      */
357     function getAlt($dbi, $argarray, $request)
358     {
359         return '<?plugin ' . $this->getName() . ' ' . $this->glueArgs($argarray) . '?>';
360     }
361
362     /**
363      * Creates HTML output to be cached.
364      * This method is only called if the plugin_type is set to
365      * PLUGIN_CACHED_HTML.
366      *
367      * @access protected pure virtual
368      * @param  dbi       WikiDB       database abstraction class
369      * @param  argarray  array        complete (!) arguments to produce
370      *                                image. It is not necessary to call
371      *                                WikiPlugin->getArgs anymore.
372      * @param  request   Request      ???
373      * @param  string $basepage Pagename to use to interpret links [/relative] page names.
374      * @return string html to be printed in place of the plugin command
375      *                                false if an error occured
376      */
377     function getHtml($dbi, $argarray, $request, $basepage)
378     {
379         trigger_error('WikiPluginCached::getHtml: pure virtual function in file '
380             . __FILE__ . ' line ' . __LINE__, E_USER_ERROR);
381     }
382
383     /**
384      * Creates HTML output to be cached.
385      * This method is only called if the plugin_type is set to
386      * PLUGIN_CACHED_HTML.
387      *
388      * @access protected pure virtual
389      * @param  dbi       WikiDB       database abstraction class
390      * @param  argarray  array        complete (!) arguments to produce
391      *                                image. It is not necessary to call
392      *                                WikiPlugin->getArgs anymore.
393      * @param  request   Request      ???
394      * @return array(html,handle) html for the map interior (to be specific,
395      *                                only &lt;area;&gt; tags defining hot spots)
396      *                                handle is an imagehandle to the corresponding
397      *                                image.
398      *                                array(false,false) if an error occured
399      */
400     function getMap($dbi, $argarray, $request)
401     {
402         trigger_error('WikiPluginCached::getHtml: pure virtual function in file '
403             . __FILE__ . ' line ' . __LINE__, E_USER_ERROR);
404     }
405
406     /* --------------------- produce Html ----------------------------- */
407
408     /**
409      * Creates an HTML map hyperlinked to the image specified
410      * by url and containing the hotspots given by map.
411      *
412      * @access private
413      * @param  id       string  unique id for the plugin call
414      * @param  url      string  url pointing to the image part of the map
415      * @param  map      string  &lt;area&gt; tags defining active
416      *                          regions in the map
417      * @param  dbi      WikiDB  database abstraction class
418      * @param  argarray array   complete (!) arguments to produce
419      *                          image. It is not necessary to call
420      *                          WikiPlugin->getArgs anymore.
421      * @param  request  Request ???
422      * @return string html output
423      */
424     function embedMap($id, $url, $map, &$dbi, $argarray, &$request)
425     {
426         // id is not unique if the same map is produced twice
427         $key = substr($id, 0, 8) . substr(microtime(), 0, 6);
428         return HTML(HTML::map(array('name' => $key), $map),
429             HTML::img(array(
430                 'src' => $url,
431                 //  'alt'    => htmlspecialchars($this->getAlt($dbi,$argarray,$request))
432                 'usemap' => '#' . $key))
433         );
434     }
435
436     /**
437      * Creates an HTML &lt;img&gt; tag hyperlinking to the specified
438      * url and produces an alternative text for non-graphical
439      * browsers.
440      *
441      * @access private
442      * @param  url      string  url pointing to the image part of the map
443      * @param  map      string  &lt;area&gt; tags defining active
444      *                          regions in the map
445      * @param  dbi      WikiDB  database abstraction class
446      * @param  argarray array   complete (!) arguments to produce
447      *                          image. It is not necessary to call
448      *                          WikiPlugin->getArgs anymore.
449      * @param  request  Request ???
450      * @return string html output
451      */
452     function embedImg($url, $dbi, $argarray, $request)
453     {
454         return HTML::img(array(
455             'src' => $url,
456             'alt' => htmlspecialchars($this->getAlt($dbi, $argarray, $request))));
457     }
458
459     /**
460      * svg?, swf, ...
461     <object type="audio/x-wav" standby="Loading Audio" data="example.wav">
462     <param name="src" value="example.wav" valuetype="data"></param>
463     <param name="autostart" value="false" valuetype="data"></param>
464     <param name="controls" value="ControlPanel" valuetype="data"></param>
465     <a href="example.wav">Example Audio File</a>
466     </object>
467      * See http://www.protocol7.com/svg-wiki/?EmbedingSvgInHTML
468     <object data="sample.svgz" type="image/svg+xml"
469     width="400" height="300">
470     <embed src="sample.svgz" type="image/svg+xml"
471     width="400" height="300" />
472     <p>Alternate Content like <img src="" /></p>
473     </object>
474      */
475     // how to handle alternate images? always provide alternate static images?
476     function embedObject($url, $type, $args = false, $params = false)
477     {
478         if (!$args) $args = array();
479         $object = HTML::object(array_merge($args, array('src' => $url, 'type' => $type)));
480         if ($params)
481             $object->pushContent($params);
482         return $object;
483     }
484
485 // --------------------------------------------------------------------------
486 // ---------------------- static member functions ---------------------------
487 // --------------------------------------------------------------------------
488
489     /**
490      * Creates one static PEAR Cache object and returns copies afterwards.
491      * FIXME: There should be references returned
492      *
493      * @access static protected
494      * @return Cache copy of the cache object
495      */
496     function newCache()
497     {
498         static $staticcache;
499
500         if (!is_object($staticcache)) {
501             if (!class_exists('Cache')) {
502                 // uuh, pear not in include_path! should print a warning.
503                 // search some possible pear paths.
504                 $pearFinder = new PearFileFinder;
505                 if ($lib = $pearFinder->findFile('Cache.php', 'missing_ok'))
506                     require_once($lib);
507                 else // fall back to our own copy
508                     require_once 'lib/pear/Cache.php';
509             }
510             $cacheparams = array();
511             foreach (explode(':', 'database:cache_dir:filename_prefix:highwater:lowwater'
512                 . ':maxlifetime:maxarglen:usecache:force_syncmap') as $key) {
513                 $cacheparams[$key] = constant('PLUGIN_CACHED_' . strtoupper($key));
514             }
515             $cacheparams['imgtypes'] = preg_split('/\s*:\s*/', PLUGIN_CACHED_IMGTYPES);
516             $staticcache = new Cache(PLUGIN_CACHED_DATABASE, $cacheparams);
517             $staticcache->gc_maxlifetime = PLUGIN_CACHED_MAXLIFETIME;
518
519             if (!PLUGIN_CACHED_USECACHE) {
520                 $staticcache->setCaching(false);
521             }
522         }
523         return $staticcache; // FIXME: use references ?
524     }
525
526     /**
527      * Determines whether a needed image type may is available
528      * from the GD library and gives an alternative otherwise.
529      *
530      * @access  public static
531      * @param   wish   string one of 'png', 'gif', 'jpeg', 'jpg'
532      * @return string the image type to be used ('png', 'gif', 'jpeg')
533      *                        'html' in case of an error
534      */
535
536     function decideImgType($wish)
537     {
538         if ($wish == 'html') return $wish;
539         if ($wish == 'jpg') {
540             $wish = 'jpeg';
541         }
542
543         $supportedtypes = array();
544         // Todo: swf, pdf, ...
545         $imagetypes = array(
546             'png' => IMG_PNG,
547             'gif' => IMG_GIF,
548             'jpeg' => IMG_JPEG,
549             'wbmp' => IMG_WBMP,
550             'xpm' => IMG_XPM,
551             /* // these do work but not with the ImageType bitmask
552             'gd'    => IMG_GD,
553             'gd2'   => IMG_GD,
554             'xbm'   => IMG_XBM,
555             */
556         );
557         if (function_exists('ImageTypes')) {
558             $presenttypes = ImageTypes();
559             foreach ($imagetypes as $imgtype => $bitmask)
560                 if ($presenttypes && $bitmask)
561                     array_push($supportedtypes, $imgtype);
562         } else {
563             foreach ($imagetypes as $imgtype => $bitmask)
564                 if (function_exists("Image" . $imgtype))
565                     array_push($supportedtypes, $imgtype);
566         }
567         if (in_array($wish, $supportedtypes))
568             return $wish;
569         elseif (!empty($supportedtypes))
570             return reset($supportedtypes); else
571             return 'html';
572
573     } // decideImgType
574
575     /**
576      * Writes an image into a file or to the browser.
577      * Note that there is no check if the image can
578      * be written.
579      *
580      * @access  public static
581      * @param   imgtype   string 'png', 'gif' or 'jpeg'
582      * @param   imghandle string image handle containing the image
583      * @param   imgfile   string file name of the image to be produced
584      * @return void
585      * @see     decideImageType
586      */
587     function writeImage($imgtype, $imghandle, $imgfile = false)
588     {
589         if ($imgtype != 'html') {
590             $func = "Image" . strtoupper($imgtype);
591             if ($imgfile) {
592                 $func($imghandle, $imgfile);
593             } else {
594                 $func($imghandle);
595             }
596         }
597     } // writeImage
598
599     /**
600      * Sends HTTP Header for some predefined file types.
601      * There is no parameter check.
602      *
603      * @access  public static
604      * @param   doctype string 'gif', 'png', 'jpeg', 'html'
605      * @return void
606      */
607     function writeHeader($doctype)
608     {
609         static $IMAGEHEADER = array(
610             'gif' => 'Content-type: image/gif',
611             'png' => 'Content-type: image/png',
612             'jpeg' => 'Content-type: image/jpeg',
613             'xbm' => 'Content-type: image/xbm',
614             'xpm' => 'Content-type: image/xpm',
615             'gd' => 'Content-type: image/gd',
616             'gd2' => 'Content-type: image/gd2',
617             'wbmp' => 'Content-type: image/vnd.wap.wbmp', // wireless bitmaps for PDA's and such.
618             'html' => 'Content-type: text/html');
619         // Todo: swf, pdf, svg, svgz
620         Header($IMAGEHEADER[$doctype]);
621     }
622
623
624     /**
625      * Converts argument array to a string of format option="value".
626      * This should only be used for displaying plugin options for
627      * the quoting of arguments is not safe, yet.
628      *
629      * @access public static
630      * @param  argarray array   contains all arguments to be converted
631      * @return string concated arguments
632      */
633     function glueArgs($argarray)
634     {
635         if (!empty($argarray)) {
636             $argstr = '';
637             while (list($key, $value) = each($argarray)) {
638                 $argstr .= $key . '=' . '"' . $value . '" ';
639                 // FIXME: How are values quoted? Can a value contain '"'?
640                 // TODO: rawurlencode(value)
641             }
642             return substr($argstr, 0, strlen($argstr) - 1);
643         }
644         return '';
645     } // glueArgs
646
647     // ---------------------- FetchImageFromCache ------------------------------
648
649     /**
650      * Extracts the cache entry id from the url and the plugin call
651      * parameters if available.
652      *
653      * @access private static
654      * @param  id           string   return value. Image is stored under this id.
655      * @param  plugincall   string   return value. Only returned if present in url.
656      *                               Contains all parameters to reconstruct
657      *                               plugin call.
658      * @param  cache        Cache    PEAR Cache object
659      * @param  request      Request  ???
660      * @param  errorformat  string   format which should be used to
661      *                               output errors ('html', 'png', 'gif', 'jpeg')
662      * @return boolean false if an error occurs, true otherwise.
663      *                               Param id and param plugincall are
664      *                               also return values.
665      */
666     function checkCall1(&$id, &$plugincall, $cache, $request, $errorformat)
667     {
668         $id = $request->getArg('id');
669         $plugincall = rawurldecode($request->getArg('args'));
670
671         if (!$id) {
672             if (!$plugincall) {
673                 // This should never happen, so do not gettextify.
674                 $errortext = "Neither 'args' nor 'id' given. Cannot proceed without parameters.";
675                 $this->printError($errorformat, $errortext);
676                 return false;
677             } else {
678                 $id = $cache->generateId($plugincall);
679             }
680         }
681         return true;
682     } // checkCall1
683
684
685     /**
686      * Extracts the parameters necessary to reconstruct the plugin
687      * call needed to produce the requested image.
688      *
689      * @access static private
690      * @param  plugincall string   reference to serialized array containing both
691      *                             name and parameters of the plugin call
692      * @param  request    Request  ???
693      * @return boolean false if an error occurs, true otherwise.
694      *
695      */
696     function checkCall2(&$plugincall, $request)
697     {
698         // if plugincall wasn't sent by URL, it must have been
699         // stored in a session var instead and we can retreive it from there
700         if (!$plugincall) {
701             if (!$plugincall = $request->getSessionVar('imagecache' . $id)) {
702                 // I think this is the only error which may occur
703                 // without having written bad code. So gettextify it.
704                 $errortext = sprintf(
705                     gettext("There is no image creation data available to id '%s'. Please reload referring page."),
706                     $id);
707                 $this->printError($errorformat, $errortext);
708                 return false;
709             }
710         }
711         $plugincall = unserialize($plugincall);
712         return true;
713     } // checkCall2
714
715
716     /**
717      * Creates an image or image map depending on the plugin type.
718      * @access static private
719      * @param  content array             reference to created array which overwrite the keys
720      *                                   'image', 'imagetype' and possibly 'html'
721      * @param  plugin  WikiPluginCached  plugin which is called to create image or map
722      * @param  dbi     WikiDB            handle to database
723      * @param  argarray array            Contains all arguments needed by plugin
724      * @param  request Request           ????
725      * @param  errorformat string        outputs errors in 'png', 'gif', 'jpg' or 'html'
726      * @return boolean error status; true=ok; false=error
727      */
728     function produceImage(&$content, $plugin, $dbi, $argarray, $request, $errorformat)
729     {
730         $plugin->resetError();
731         $content['html'] = $imagehandle = false;
732         if ($plugin->getPluginType() == PLUGIN_CACHED_MAP) {
733             list($imagehandle, $content['html']) = $plugin->getMap($dbi, $argarray, $request);
734         } else {
735             $imagehandle = $plugin->getImage($dbi, $argarray, $request);
736         }
737
738         $content['imagetype']
739             = $this->decideImgType($plugin->getImageType($dbi, $argarray, $request));
740         $errortext = $plugin->getError();
741
742         if (!$imagehandle || $errortext) {
743             if (!$errortext) {
744                 $errortext = "'<?plugin " . $plugin->getName() . ' '
745                     . $this->glueArgs($argarray) . " ?>' returned no image, "
746                     . " although no error was reported.";
747             }
748             $this->printError($errorformat, $errortext);
749             return false;
750         }
751
752         // image handle -> image data
753         if (!empty($this->_static)) {
754             $ext = "." . $content['imagetype'];
755             if (is_string($imagehandle) and file_exists($imagehandle)) {
756                 if (preg_match("/.(\w+)$/", $imagehandle, $m)) {
757                     $ext = "." . $m[1];
758                 }
759             }
760             $tmpfile = tempnam(getUploadFilePath(), PLUGIN_CACHED_FILENAME_PREFIX . $ext);
761             if (!strstr(basename($tmpfile), $ext)) {
762                 unlink($tmpfile);
763                 $tmpfile .= $ext;
764             }
765             $tmpfile = getUploadFilePath() . basename($tmpfile);
766             if (is_string($imagehandle) and file_exists($imagehandle)) {
767                 rename($imagehandle, $tmpfile);
768             }
769         } else {
770             $tmpfile = $this->tempnam();
771         }
772         if (is_resource($imagehandle)) {
773             $this->writeImage($content['imagetype'], $imagehandle, $tmpfile);
774             ImageDestroy($imagehandle);
775             sleep(0.2);
776         } elseif (is_string($imagehandle)) {
777             $content['file'] = getUploadFilePath() . basename($tmpfile);
778             $content['url'] = getUploadDataPath() . basename($tmpfile);
779             return true;
780         }
781         if (file_exists($tmpfile)) {
782             $fp = fopen($tmpfile, 'rb');
783             $content['image'] = fread($fp, filesize($tmpfile));
784             fclose($fp);
785             if (!empty($this->_static)) {
786                 // on static it is in "uploads/" but in wikicached also
787                 $content['file'] = $tmpfile;
788                 $content['url'] = getUploadDataPath() . basename($tmpfile);
789                 return true;
790             }
791             unlink($tmpfile);
792             if ($content['image'])
793                 return true;
794         }
795         return false;
796     }
797
798     function staticUrl($tmpfile)
799     {
800         $content['file'] = $tmpfile;
801         $content['url'] = getUploadDataPath() . basename($tmpfile);
802         return $content;
803     }
804
805     function tempnam($prefix = "")
806     {
807         if (preg_match("/^(.+)\.(\w{2,4})$/", $prefix, $m)) {
808             $prefix = $m[1];
809             $ext = "." . $m[2];
810         } else {
811             $ext = isWindows() ? ".tmp" : "";
812         }
813         $temp = tempnam(isWindows() ? str_replace('/', "\\", PLUGIN_CACHED_CACHE_DIR)
814                 : PLUGIN_CACHED_CACHE_DIR,
815             $prefix ? $prefix : PLUGIN_CACHED_FILENAME_PREFIX);
816         if (isWindows()) {
817             if ($ext != ".tmp") unlink($temp);
818             $temp = preg_replace("/\.tmp$/", $ext, $temp);
819         } else {
820             $temp .= $ext;
821         }
822         return $temp;
823     }
824
825     /**
826      * Main function for obtaining images from cache or generating on-the-fly
827      * from parameters sent by url or session vars.
828      *
829      * @access static public
830      * @param  dbi     WikiDB            handle to database
831      * @param  request Request           ???
832      * @param  errorformat string        outputs errors in 'png', 'gif', 'jpeg' or 'html'
833      */
834     function fetchImageFromCache($dbi, $request, $errorformat = 'png')
835     {
836         $cache = $this->newCache();
837         $errorformat = $this->decideImgType($errorformat);
838         // get id
839         if (!$this->checkCall1($id, $plugincall, $cache, $request, $errorformat)) return false;
840         // check cache
841         $content = $cache->get($id, 'imagecache');
842
843         if (!empty($content['image'])) {
844             $this->writeHeader($content['imagetype']);
845             print $content['image'];
846             return true;
847         }
848         if (!empty($content['html'])) {
849             print $content['html'];
850             return true;
851         }
852         // static version?
853         if (!empty($content['file']) && !empty($content['url']) && file_exists($content['file'])) {
854             print $this->embedImg($content['url'], $dbi, array(), $request);
855             return true;
856         }
857
858         // re-produce image. At first, we need the plugincall parameters.
859         // Cached args with matching id override given args to shorten getimg.php?id=md5
860         if (!empty($content['args']))
861             $plugincall['arguments'] = $content['args'];
862         if (!$this->checkCall2($plugincall, $request)) return false;
863
864         $pluginname = $plugincall['pluginname'];
865         $argarray = $plugincall['arguments'];
866
867         $loader = new WikiPluginLoader;
868         $plugin = $loader->getPlugin($pluginname);
869
870         // cache empty, but image maps have to be created _inline_
871         // so ask user to reload wiki page instead
872         if (($plugin->getPluginType() & PLUGIN_CACHED_MAP) && PLUGIN_CACHED_FORCE_SYNCMAP) {
873             $errortext = _("Image map expired. Reload wiki page to recreate its html part.");
874             $this->printError($errorformat, $errortext);
875         }
876
877         if (!$this->produceImage($content, $plugin, $dbi, $argarray,
878             $request, $errorformat)
879         )
880             return false;
881
882         $expire = $plugin->getExpire($dbi, $argarray, $request);
883
884         if ($content['image']) {
885             $cache->save($id, $content, $expire, 'imagecache');
886             $this->writeHeader($content['imagetype']);
887             print $content['image'];
888             return true;
889         }
890
891         $errortext = "Could not create image file from imagehandle.";
892         $this->printError($errorformat, $errortext);
893         return false;
894     } // FetchImageFromCache
895
896     // -------------------- error handling ----------------------------
897
898     /**
899      * Resets buffer containing all error messages. This is allways
900      * done before invoking any abstract creation routines like
901      * <code>getImage</code>.
902      *
903      * @access private
904      * @return void
905      */
906     function resetError()
907     {
908         $this->_errortext = '';
909     }
910
911     /**
912      * Returns all accumulated error messages.
913      *
914      * @access protected
915      * @return string error messages printed with <code>complain</code>.
916      */
917     function getError()
918     {
919         return $this->_errortext;
920     }
921
922     /**
923      * Collects the error messages in a string for later output
924      * by WikiPluginCached. This should be used for any errors
925      * that occur during data (html,image,map) creation.
926      *
927      * @access protected
928      * @param  addtext string errormessage to be printed (separate
929      *                        multiple lines with '\n')
930      * @return void
931      */
932     function complain($addtext)
933     {
934         $this->_errortext .= $addtext;
935     }
936
937     /**
938      * Outputs the error as image if possible or as html text
939      * if wished or html header has already been sent.
940      *
941      * @access static protected
942      * @param  imgtype string 'png', 'gif', 'jpeg' or 'html'
943      * @param  errortext string guess what?
944      * @return void
945      */
946     function printError($imgtype, $errortext)
947     {
948         $imgtype = $this->decideImgType($imgtype);
949
950         $talkedallready = ob_get_contents() || headers_sent();
951         if (($imgtype == 'html') || $talkedallready) {
952             if (is_object($errortext))
953                 $errortext = $errortext->asXml();
954             trigger_error($errortext, E_USER_WARNING);
955         } else {
956             $red = array(255, 0, 0);
957             $grey = array(221, 221, 221);
958             if (is_object($errortext))
959                 $errortext = $errortext->asString();
960             $im = $this->text2img($errortext, 2, $red, $grey);
961             if (!$im) {
962                 trigger_error($errortext, E_USER_WARNING);
963                 return;
964             }
965             $this->writeHeader($imgtype);
966             $this->writeImage($imgtype, $im);
967             ImageDestroy($im);
968         }
969     } // printError
970
971
972     /**
973      * Basic text to image converter for error handling which allows
974      * multiple line output.
975      * It will only output the first 25 lines of 80 characters. Both
976      * values may be smaller if the chosen font is to big for there
977      * is further restriction to 600 pixel in width and 350 in height.
978      *
979      * @access static public
980      * @param  txt     string  multi line text to be converted
981      * @param  fontnr  integer number (1-5) telling gd which internal font to use;
982      *                         I recommend font 2 for errors and 4 for help texts.
983      * @param  textcol array   text color as a list of the rgb components; array(red,green,blue)
984      * @param  bgcol   array   background color; array(red,green,blue)
985      * @return string image handle for gd routines
986      */
987     function text2img($txt, $fontnr, $textcol, $bgcol)
988     {
989         // basic (!) output for error handling
990
991         // check parameters
992         if ($fontnr < 1 || $fontnr > 5) {
993             $fontnr = 2;
994         }
995         if (!is_array($textcol) || !is_array($bgcol)) {
996             $textcol = array(0, 0, 0);
997             $bgcol = array(255, 255, 255);
998         }
999         foreach (array_merge($textcol, $bgcol) as $component) {
1000             if ($component < 0 || $component > 255) {
1001                 $textcol = array(0, 0, 0);
1002                 $bgcol = array(255, 255, 255);
1003                 break;
1004             }
1005         }
1006
1007         // prepare Parameters
1008
1009         // set maximum values
1010         $IMAGESIZE = array(
1011             'cols' => 80,
1012             'rows' => 25,
1013             'width' => 600,
1014             'height' => 350);
1015
1016         if (function_exists('ImageFontWidth')) {
1017             $charx = ImageFontWidth($fontnr);
1018             $chary = ImageFontHeight($fontnr);
1019         } else {
1020             $charx = 10;
1021             $chary = 10;
1022         }
1023         $marginx = $charx;
1024         $marginy = floor($chary / 2);
1025
1026         $IMAGESIZE['cols'] = min($IMAGESIZE['cols'], floor(($IMAGESIZE['width'] - 2 * $marginx) / $charx));
1027         $IMAGESIZE['rows'] = min($IMAGESIZE['rows'], floor(($IMAGESIZE['height'] - 2 * $marginy) / $chary));
1028
1029         // split lines
1030         $y = 0;
1031         $wx = 0;
1032         do {
1033             $len = strlen($txt);
1034             $npos = strpos($txt, "\n");
1035
1036             if ($npos === false) {
1037                 $breaklen = min($IMAGESIZE['cols'], $len);
1038             } else {
1039                 $breaklen = min($npos + 1, $IMAGESIZE['cols']);
1040             }
1041             $lines[$y] = chop(substr($txt, 0, $breaklen));
1042             $wx = max($wx, strlen($lines[$y++]));
1043             $txt = substr($txt, $breaklen);
1044         } while ($txt && ($y < $IMAGESIZE['rows']));
1045
1046         // recalculate image size
1047         $IMAGESIZE['rows'] = $y;
1048         $IMAGESIZE['cols'] = $wx;
1049
1050         $IMAGESIZE['width'] = $IMAGESIZE['cols'] * $charx + 2 * $marginx;
1051         $IMAGESIZE['height'] = $IMAGESIZE['rows'] * $chary + 2 * $marginy;
1052
1053         // create blank image
1054         $im = @ImageCreate($IMAGESIZE['width'], $IMAGESIZE['height']);
1055
1056         $col = ImageColorAllocate($im, $textcol[0], $textcol[1], $textcol[2]);
1057         $bg = ImageColorAllocate($im, $bgcol[0], $bgcol[1], $bgcol[2]);
1058
1059         ImageFilledRectangle($im, 0, 0, $IMAGESIZE['width'] - 1, $IMAGESIZE['height'] - 1, $bg);
1060
1061         // write text lines
1062         foreach ($lines as $nr => $textstr) {
1063             ImageString($im, $fontnr, $marginx, $marginy + $nr * $chary,
1064                 $textstr, $col);
1065         }
1066         return $im;
1067     } // text2img
1068
1069     function newFilterThroughCmd($input, $commandLine)
1070     {
1071         $descriptorspec = array(
1072             0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1073             1 => array("pipe", "w"), // stdout is a pipe that the child will write to
1074             2 => array("pipe", "w"), // stdout is a pipe that the child will write to
1075         );
1076
1077         $process = proc_open("$commandLine", $descriptorspec, $pipes);
1078         if (is_resource($process)) {
1079             // $pipes now looks like this:
1080             // 0 => writeable handle connected to child stdin
1081             // 1 => readable  handle connected to child stdout
1082             // 2 => readable  handle connected to child stderr
1083             fwrite($pipes[0], $input);
1084             fclose($pipes[0]);
1085             $buf = "";
1086             while (!feof($pipes[1])) {
1087                 $buf .= fgets($pipes[1], 1024);
1088             }
1089             fclose($pipes[1]);
1090             $stderr = '';
1091             while (!feof($pipes[2])) {
1092                 $stderr .= fgets($pipes[2], 1024);
1093             }
1094             fclose($pipes[2]);
1095             // It is important that you close any pipes before calling
1096             // proc_close in order to avoid a deadlock
1097             $return_value = proc_close($process);
1098             if (empty($buf)) printXML($this->error($stderr));
1099             return $buf;
1100         }
1101     }
1102
1103     // run "echo $source | $commandLine" and return result
1104     function filterThroughCmd($source, $commandLine)
1105     {
1106         return $this->newFilterThroughCmd($source, $commandLine);
1107     }
1108
1109     /**
1110      * Execute system command and wait until the outfile $until exists.
1111      *
1112      * @param  cmd   string   command to be invoked
1113      * @param  until string   expected output filename
1114      * @return boolean error status; true=ok; false=error
1115      */
1116     function execute($cmd, $until = false)
1117     {
1118         // cmd must redirect stderr to stdout though!
1119         $errstr = exec($cmd); //, $outarr, $returnval); // normally 127
1120         //$errstr = join('',$outarr);
1121         $ok = empty($errstr);
1122         if (!$ok) {
1123             trigger_error("\n" . $cmd . " failed: $errstr", E_USER_WARNING);
1124         } elseif ($GLOBALS['request']->getArg('debug'))
1125             trigger_error("\n" . $cmd . ": success\n", E_USER_NOTICE);
1126         if (!isWindows()) {
1127             if ($until) {
1128                 $loop = 100000;
1129                 while (!file_exists($until) and $loop > 0) {
1130                     $loop -= 100;
1131                     usleep(100);
1132                 }
1133             } else {
1134                 usleep(5000);
1135             }
1136         }
1137         if ($until)
1138             return file_exists($until);
1139         return $ok;
1140     }
1141
1142 }
1143
1144 // Local Variables:
1145 // mode: php
1146 // tab-width: 8
1147 // c-basic-offset: 4
1148 // c-hanging-comment-ender-p: nil
1149 // indent-tabs-mode: nil
1150 // End: