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