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