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