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