Ejemplo n.º 1
0
 /**
  * Output the stored data
  *
  * If outputted in an aggregation we collect the images into a gallery.
  *
  * @param string|int $value the value stored in the database
  * @param \Doku_Renderer $R the renderer currently used to render the data
  * @param string $mode The mode the output is rendered in (eg. XHTML)
  * @return bool true if $mode could be satisfied
  */
 public function renderValue($value, \Doku_Renderer $R, $mode)
 {
     // get width and height from config
     $width = null;
     $height = null;
     if ($this->config['width']) {
         $width = $this->config['width'];
     }
     if ($this->config['height']) {
         $height = $this->config['height'];
     }
     if (!empty($R->info['struct_table_hash'])) {
         // this is an aggregation, check for special values
         if ($this->config['agg_width']) {
             $width = $this->config['agg_width'];
         }
         if ($this->config['agg_height']) {
             $height = $this->config['agg_height'];
         }
     }
     // depending on renderer type directly output or get value from it
     $returnLink = null;
     $html = '';
     if (!media_isexternal($value)) {
         if (is_a($R, '\\Doku_Renderer_xhtml')) {
             /** @var \Doku_Renderer_xhtml $R */
             $html = $R->internalmedia($value, null, null, $width, $height, null, 'direct', true);
         } else {
             $R->internalmedia($value, null, null, $width, $height, null, 'direct');
         }
     } else {
         if (is_a($R, '\\Doku_Renderer_xhtml')) {
             /** @var \Doku_Renderer_xhtml $R */
             $html = $R->externalmedia($value, null, null, $width, $height, null, 'direct', true);
         } else {
             $R->externalmedia($value, null, null, $width, $height, null, 'direct');
         }
     }
     // add gallery meta data in XHTML
     if ($mode == 'xhtml') {
         list(, $mime, ) = mimetype($value, false);
         if (substr($mime, 0, 6) == 'image/') {
             $hash = !empty($R->info['struct_table_hash']) ? "[gal-" . $R->info['struct_table_hash'] . "]" : '';
             $html = str_replace('href', "rel=\"lightbox{$hash}\" href", $html);
         }
         $R->doc .= $html;
     }
     return true;
 }
Ejemplo n.º 2
0
 /**
  * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
  * which returns an existing media revision less or equal to rev or date_at
  *
  * @author lisps
  * @param string $media_id
  * @access protected
  * @return string revision ('' for current)
  */
 function _getLastMediaRevisionAt($media_id)
 {
     if (!$this->date_at || media_isexternal($media_id)) {
         return '';
     }
     $pagelog = new MediaChangeLog($media_id);
     return $pagelog->getLastRevisionAt($this->date_at);
 }
 /**
  * Override the mpdf _getImage function
  *
  * This function takes care of gathering the image data from HTTP or
  * local files before passing the data back to mpdf's original function
  * making sure that only cached file paths are passed to mpdf. It also
  * takes care of checking image ACls.
  */
 function _getImage(&$file, $firsttime = true, $allowvector = true, $orig_srcpath = false)
 {
     global $conf;
     // build regex to parse URL back to media info
     $re = preg_quote(ml('xxx123yyy', '', true, '&', true), '/');
     $re = str_replace('xxx123yyy', '([^&\\?]*)', $re);
     // extract the real media from a fetch.php uri and determine mime
     if (preg_match("/^{$re}/", $file, $m) || preg_match('/[&\\?]media=([^&\\?]*)/', $file, $m)) {
         $media = rawurldecode($m[1]);
         list($ext, $mime) = mimetype($media);
     } else {
         list($ext, $mime) = mimetype($file);
     }
     // local files
     $local = '';
     if (substr($file, 0, 9) == 'dw2pdf://') {
         // support local files passed from plugins
         $local = substr($file, 9);
     } elseif (!preg_match('/(\\.php|\\?)/', $file)) {
         $re = preg_quote(DOKU_URL, '/');
         // directly access local files instead of using HTTP, skip dynamic content
         $local = preg_replace("/^{$re}/i", DOKU_INC, $file);
     }
     if (substr($mime, 0, 6) == 'image/') {
         if (!empty($media)) {
             // any size restrictions?
             $w = $h = 0;
             if (preg_match('/[\\?&]w=(\\d+)/', $file, $m)) {
                 $w = $m[1];
             }
             if (preg_match('/[\\?&]h=(\\d+)/', $file, $m)) {
                 $h = $m[1];
             }
             if (media_isexternal($media)) {
                 $local = media_get_from_URL($media, $ext, -1);
                 if (!$local) {
                     $local = $media;
                 }
                 // let mpdf try again
             } else {
                 $media = cleanID($media);
                 //check permissions (namespace only)
                 if (auth_quickaclcheck(getNS($media) . ':X') < AUTH_READ) {
                     $file = '';
                 }
                 $local = mediaFN($media);
             }
             //handle image resizing/cropping
             if ($w && file_exists($local)) {
                 if ($h) {
                     $local = media_crop_image($local, $ext, $w, $h);
                 } else {
                     $local = media_resize_image($local, $ext, $w, $h);
                 }
             }
         } elseif (media_isexternal($file)) {
             // fixed external URLs
             $local = media_get_from_URL($file, $ext, $conf['cachetime']);
         }
         if ($local) {
             $file = $local;
             $orig_srcpath = $local;
         }
     }
     return parent::_getImage($file, $firsttime, $allowvector, $orig_srcpath);
 }
Ejemplo n.º 4
0
 public function test_internal()
 {
     $this->assertFalse(media_isexternal('wiki:logo.png'));
     $this->assertFalse(media_isexternal('private:logo.png'));
     $this->assertFalse(media_isexternal('ftp:private:logo.png'));
 }
Ejemplo n.º 5
0
/**
 * Build a link to a media file
 *
 * Will return a link to the detail page if $direct is false
 *
 * The $more parameter should always be given as array, the function then
 * will strip default parameters to produce even cleaner URLs
 *
 * @param string  $id     the media file id or URL
 * @param mixed   $more   string or array with additional parameters
 * @param bool    $direct link to detail page if false
 * @param string  $sep    URL parameter separator
 * @param bool    $abs    Create an absolute URL
 * @return string
 */
function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false)
{
    global $conf;
    $isexternalimage = media_isexternal($id);
    if (!$isexternalimage) {
        $id = cleanID($id);
    }
    if (is_array($more)) {
        // add token for resized images
        if (!empty($more['w']) || !empty($more['h']) || $isexternalimage) {
            $more['tok'] = media_get_token($id, $more['w'], $more['h']);
        }
        // strip defaults for shorter URLs
        if (isset($more['cache']) && $more['cache'] == 'cache') {
            unset($more['cache']);
        }
        if (empty($more['w'])) {
            unset($more['w']);
        }
        if (empty($more['h'])) {
            unset($more['h']);
        }
        if (isset($more['id']) && $direct) {
            unset($more['id']);
        }
        if (isset($more['rev']) && !$more['rev']) {
            unset($more['rev']);
        }
        $more = buildURLparams($more, $sep);
    } else {
        $matches = array();
        if (preg_match_all('/\\b(w|h)=(\\d*)\\b/', $more, $matches, PREG_SET_ORDER) || $isexternalimage) {
            $resize = array('w' => 0, 'h' => 0);
            foreach ($matches as $match) {
                $resize[$match[1]] = $match[2];
            }
            $more .= $more === '' ? '' : $sep;
            $more .= 'tok=' . media_get_token($id, $resize['w'], $resize['h']);
        }
        $more = str_replace('cache=cache', '', $more);
        //skip default
        $more = str_replace(',,', ',', $more);
        $more = str_replace(',', $sep, $more);
    }
    if ($abs) {
        $xlink = DOKU_URL;
    } else {
        $xlink = DOKU_BASE;
    }
    // external URLs are always direct without rewriting
    if ($isexternalimage) {
        $xlink .= 'lib/exe/fetch.php';
        $xlink .= '?' . $more;
        $xlink .= $sep . 'media=' . rawurlencode($id);
        return $xlink;
    }
    $id = idfilter($id);
    // decide on scriptname
    if ($direct) {
        if ($conf['userewrite'] == 1) {
            $script = '_media';
        } else {
            $script = 'lib/exe/fetch.php';
        }
    } else {
        if ($conf['userewrite'] == 1) {
            $script = '_detail';
        } else {
            $script = 'lib/exe/detail.php';
        }
    }
    // build URL based on rewrite mode
    if ($conf['userewrite']) {
        $xlink .= $script . '/' . $id;
        if ($more) {
            $xlink .= '?' . $more;
        }
    } else {
        if ($more) {
            $xlink .= $script . '?' . $more;
            $xlink .= $sep . 'media=' . $id;
        } else {
            $xlink .= $script . '?media=' . $id;
        }
    }
    return $xlink;
}
Ejemplo n.º 6
0
/**
 * Check for media for preconditions and return correct status code
 *
 * READ: MEDIA, MIME, EXT, CACHE
 * WRITE: MEDIA, FILE, array( STATUS, STATUSMESSAGE )
 *
 * @author Gerry Weissbach <*****@*****.**>
 *
 * @param string $media  reference to the media id
 * @param string $file   reference to the file variable
 * @param string $rev
 * @param int    $width
 * @param int    $height
 * @return array as array(STATUS, STATUSMESSAGE)
 */
function checkFileStatus(&$media, &$file, $rev = '', $width = 0, $height = 0)
{
    global $MIME, $EXT, $CACHE, $INPUT;
    //media to local file
    if (media_isexternal($media)) {
        //check token for external image and additional for resized and cached images
        if (media_get_token($media, $width, $height) !== $INPUT->str('tok')) {
            return array(412, 'Precondition Failed');
        }
        //handle external images
        if (strncmp($MIME, 'image/', 6) == 0) {
            $file = media_get_from_URL($media, $EXT, $CACHE);
        }
        if (!$file) {
            //download failed - redirect to original URL
            return array(302, $media);
        }
    } else {
        $media = cleanID($media);
        if (empty($media)) {
            return array(400, 'Bad request');
        }
        // check token for resized images
        if (($width || $height) && media_get_token($media, $width, $height) !== $INPUT->str('tok')) {
            return array(412, 'Precondition Failed');
        }
        //check permissions (namespace only)
        if (auth_quickaclcheck(getNS($media) . ':X') < AUTH_READ) {
            return array(403, 'Forbidden');
        }
        $file = mediaFN($media, $rev);
    }
    //check file existance
    if (!file_exists($file)) {
        return array(404, 'Not Found');
    }
    return array(200, null);
}
Ejemplo n.º 7
0
 /**
  * Set the text and HTML body and apply replacements
  *
  * This function applies a whole bunch of default replacements in addition
  * to the ones specified as parameters
  *
  * If you pass the HTML part or HTML replacements yourself you have to make
  * sure you encode all HTML special chars correctly
  *
  * @param string $text     plain text body
  * @param array  $textrep  replacements to apply on the text part
  * @param array  $htmlrep  replacements to apply on the HTML part, leave null to use $textrep
  * @param string $html     the HTML body, leave null to create it from $text
  * @param bool   $wrap     wrap the HTML in the default header/Footer
  */
 public function setBody($text, $textrep = null, $htmlrep = null, $html = null, $wrap = true)
 {
     $htmlrep = (array) $htmlrep;
     $textrep = (array) $textrep;
     // create HTML from text if not given
     if (is_null($html)) {
         $html = $text;
         $html = hsc($html);
         $html = preg_replace('/^----+$/m', '<hr >', $html);
         $html = nl2br($html);
     }
     if ($wrap) {
         $wrap = rawLocale('mailwrap', 'html');
         $html = preg_replace('/\\n-- <br \\/>.*$/s', '', $html);
         //strip signature
         $html = str_replace('@EMAILSIGNATURE@', '', $html);
         //strip @EMAILSIGNATURE@
         $html = str_replace('@HTMLBODY@', $html, $wrap);
     }
     if (strpos($text, '@EMAILSIGNATURE@') === false) {
         $text .= '@EMAILSIGNATURE@';
     }
     // copy over all replacements missing for HTML (autolink URLs)
     foreach ($textrep as $key => $value) {
         if (isset($htmlrep[$key])) {
             continue;
         }
         if (media_isexternal($value)) {
             $htmlrep[$key] = '<a href="' . hsc($value) . '">' . hsc($value) . '</a>';
         } else {
             $htmlrep[$key] = hsc($value);
         }
     }
     // embed media from templates
     $html = preg_replace_callback('/@MEDIA\\(([^\\)]+)\\)@/', array($this, 'autoembed_cb'), $html);
     // add default token replacements
     $trep = array_merge($this->replacements['text'], (array) $textrep);
     $hrep = array_merge($this->replacements['html'], (array) $htmlrep);
     // Apply replacements
     foreach ($trep as $key => $substitution) {
         $text = str_replace('@' . strtoupper($key) . '@', $substitution, $text);
     }
     foreach ($hrep as $key => $substitution) {
         $html = str_replace('@' . strtoupper($key) . '@', $substitution, $html);
     }
     $this->setHTML($html);
     $this->setText($text);
 }
Ejemplo n.º 8
0
 /**
  * Set the text and HTML body and apply replacements
  *
  * This function applies a whole bunch of default replacements in addition
  * to the ones specidifed as parameters
  *
  * If you pass the HTML part or HTML replacements yourself you have to make
  * sure you encode all HTML special chars correctly
  *
  * @param string $text     plain text body
  * @param array  $textrep  replacements to apply on the text part
  * @param array  $htmlrep  replacements to apply on the HTML part, leave null to use $textrep
  * @param string $html     the HTML body, leave null to create it from $text
  * @param bool   $wrap     wrap the HTML in the default header/Footer
  */
 public function setBody($text, $textrep = null, $htmlrep = null, $html = null, $wrap = true)
 {
     global $INFO;
     global $conf;
     /* @var Input $INPUT */
     global $INPUT;
     $htmlrep = (array) $htmlrep;
     $textrep = (array) $textrep;
     // create HTML from text if not given
     if (is_null($html)) {
         $html = $text;
         $html = hsc($html);
         $html = preg_replace('/^-----*$/m', '<hr >', $html);
         $html = nl2br($html);
     }
     if ($wrap) {
         $wrap = rawLocale('mailwrap', 'html');
         $html = preg_replace('/\\n-- <br \\/>.*$/s', '', $html);
         //strip signature
         $html = str_replace('@HTMLBODY@', $html, $wrap);
     }
     // copy over all replacements missing for HTML (autolink URLs)
     foreach ($textrep as $key => $value) {
         if (isset($htmlrep[$key])) {
             continue;
         }
         if (media_isexternal($value)) {
             $htmlrep[$key] = '<a href="' . hsc($value) . '">' . hsc($value) . '</a>';
         } else {
             $htmlrep[$key] = hsc($value);
         }
     }
     // embed media from templates
     $html = preg_replace_callback('/@MEDIA\\(([^\\)]+)\\)@/', array($this, 'autoembed_cb'), $html);
     // prepare default replacements
     $ip = clientIP();
     $cip = gethostsbyaddrs($ip);
     $trep = array('DATE' => dformat(), 'BROWSER' => $INPUT->server->str('HTTP_USER_AGENT'), 'IPADDRESS' => $ip, 'HOSTNAME' => $cip, 'TITLE' => $conf['title'], 'DOKUWIKIURL' => DOKU_URL, 'USER' => $INPUT->server->str('REMOTE_USER'), 'NAME' => $INFO['userinfo']['name'], 'MAIL' => $INFO['userinfo']['mail']);
     $trep = array_merge($trep, (array) $textrep);
     $hrep = array('DATE' => '<i>' . hsc(dformat()) . '</i>', 'BROWSER' => hsc($INPUT->server->str('HTTP_USER_AGENT')), 'IPADDRESS' => '<code>' . hsc($ip) . '</code>', 'HOSTNAME' => '<code>' . hsc($cip) . '</code>', 'TITLE' => hsc($conf['title']), 'DOKUWIKIURL' => '<a href="' . DOKU_URL . '">' . DOKU_URL . '</a>', 'USER' => hsc($INPUT->server->str('REMOTE_USER')), 'NAME' => hsc($INFO['userinfo']['name']), 'MAIL' => '<a href="mailto:"' . hsc($INFO['userinfo']['mail']) . '">' . hsc($INFO['userinfo']['mail']) . '</a>');
     $hrep = array_merge($hrep, (array) $htmlrep);
     // Apply replacements
     foreach ($trep as $key => $substitution) {
         $text = str_replace('@' . strtoupper($key) . '@', $substitution, $text);
     }
     foreach ($hrep as $key => $substitution) {
         $html = str_replace('@' . strtoupper($key) . '@', $substitution, $html);
     }
     $this->setHTML($html);
     $this->setText($text);
 }
 function _recordMediaUsage($src)
 {
     global $ID;
     list($src, $hash) = explode('#', $src, 2);
     if (media_isexternal($src)) {
         return;
     }
     resolve_mediaid(getNS($ID), $src, $exists);
     $this->meta['relation']['media'][$src] = $exists;
 }
Ejemplo n.º 10
0
/**
 * Calculate a token to be used to verify fetch requests for resized or
 * cropped images have been internally generated - and prevent external
 * DDOS attacks via fetch
 *
 * @author Christopher Smith <*****@*****.**>
 *
 * @param string  $id    id of the image
 * @param int     $w     resize/crop width
 * @param int     $h     resize/crop height
 * @return string
 */
function media_get_token($id, $w, $h)
{
    // token is only required for modified images
    if ($w || $h || media_isexternal($id)) {
        $token = $id;
        if ($w) {
            $token .= '.' . $w;
        }
        if ($h) {
            $token .= '.' . $h;
        }
        return substr(PassHash::hmac('md5', $token, auth_cookiesalt()), 0, 6);
    }
    return '';
}
Ejemplo n.º 11
0
function Doku_Handler_Parse_Media($match)
{
    // Strip the opening and closing markup
    $link = preg_replace(array('/^\\{\\{/', '/\\}\\}$/u'), '', $match);
    // Split title from URL
    $link = explode('|', $link, 2);
    // Check alignment
    $ralign = (bool) preg_match('/^ /', $link[0]);
    $lalign = (bool) preg_match('/ $/', $link[0]);
    // Logic = what's that ;)...
    if ($lalign & $ralign) {
        $align = 'center';
    } else {
        if ($ralign) {
            $align = 'right';
        } else {
            if ($lalign) {
                $align = 'left';
            } else {
                $align = null;
            }
        }
    }
    // The title...
    if (!isset($link[1])) {
        $link[1] = null;
    }
    //remove aligning spaces
    $link[0] = trim($link[0]);
    //split into src and parameters (using the very last questionmark)
    $pos = strrpos($link[0], '?');
    if ($pos !== false) {
        $src = substr($link[0], 0, $pos);
        $param = substr($link[0], $pos + 1);
    } else {
        $src = $link[0];
        $param = '';
    }
    //parse width and height
    if (preg_match('#(\\d+)(x(\\d+))?#i', $param, $size)) {
        !empty($size[1]) ? $w = $size[1] : ($w = null);
        !empty($size[3]) ? $h = $size[3] : ($h = null);
    } else {
        $w = null;
        $h = null;
    }
    //get linking command
    if (preg_match('/nolink/i', $param)) {
        $linking = 'nolink';
    } else {
        if (preg_match('/direct/i', $param)) {
            $linking = 'direct';
        } else {
            if (preg_match('/linkonly/i', $param)) {
                $linking = 'linkonly';
            } else {
                $linking = 'details';
            }
        }
    }
    //get caching command
    if (preg_match('/(nocache|recache)/i', $param, $cachemode)) {
        $cache = $cachemode[1];
    } else {
        $cache = 'cache';
    }
    // Check whether this is a local or remote image
    if (media_isexternal($src)) {
        $call = 'externalmedia';
    } else {
        $call = 'internalmedia';
    }
    $params = array('type' => $call, 'src' => $src, 'title' => $link[1], 'align' => $align, 'width' => $w, 'height' => $h, 'cache' => $cache, 'linking' => $linking);
    return $params;
}
Ejemplo n.º 12
0
 /**
  * Return formated data, depending on column type
  *
  * @param array               $column
  * @param string              $value
  * @param Doku_Renderer       $R
  * @return string
  */
 function _formatDataNew($column, $value, Doku_Renderer $R)
 {
     global $conf;
     $vals = explode("\n", $value);
     $outs = array();
     //multivalued line from db result for pageid and wiki has only in first value the ID
     $storedID = '';
     foreach ($vals as $val) {
         $val = trim($val);
         if ($val == '') {
             continue;
         }
         $type = $column['type'];
         if (is_array($type)) {
             $type = $type['type'];
         }
         switch ($type) {
             case 'page':
                 $val = $this->_addPrePostFixes($column['type'], $val);
                 $val = $this->ensureAbsoluteId($val);
                 $outs[] = $R->internallink($val, null, null, true);
                 break;
             case 'title':
                 list($id, $title) = explode('|', $val, 2);
                 $id = $this->_addPrePostFixes($column['type'], $id);
                 $id = $this->ensureAbsoluteId($id);
                 $outs[] = $R->internallink($id, $title, null, true);
                 break;
             case 'pageid':
                 list($id, $title) = explode('|', $val, 2);
                 //use ID from first value of the multivalued line
                 if ($title == null) {
                     $title = $id;
                     if (!empty($storedID)) {
                         $id = $storedID;
                     }
                 } else {
                     $storedID = $id;
                 }
                 $id = $this->_addPrePostFixes($column['type'], $id);
                 $outs[] = $R->internallink($id, $title, null, true);
                 break;
             case 'nspage':
                 // no prefix/postfix here
                 $val = ':' . $column['key'] . ":{$val}";
                 $outs[] = $R->internallink($val, null, null, true);
                 break;
             case 'mail':
                 list($id, $title) = explode(' ', $val, 2);
                 $id = $this->_addPrePostFixes($column['type'], $id);
                 if (!$title) {
                     $title = $id;
                 } else {
                     $title = hsc($title);
                 }
                 $outs[] = $R->emaillink($id, $title, true);
                 break;
             case 'url':
                 $val = $this->_addPrePostFixes($column['type'], $val);
                 $outs[] = $R->externallink($val, null, true);
                 break;
             case 'tag':
                 // per default use keyname as target page, but prefix on aliases
                 if (!is_array($column['type'])) {
                     $target = $column['key'] . ':';
                 } else {
                     $target = $this->_addPrePostFixes($column['type'], '');
                 }
                 $params = buildURLparams($this->_getTagUrlparam($column, $val));
                 $url = str_replace('/', ':', cleanID($target)) . '?' . $params;
                 // FIXME: The title is lost when moving to $R->internallink,
                 //        but this syntax is required to support different renderers.
                 // $title = sprintf($this->getLang('tagfilter'), hsc($val));
                 $outs[] = $R->internallink($url, hsc($val), true);
                 break;
             case 'timestamp':
                 $outs[] = dformat($val);
                 break;
             case 'wiki':
                 global $ID;
                 $oldid = $ID;
                 list($ID, $data) = explode('|', $val, 2);
                 //use ID from first value of the multivalued line
                 if ($data == null) {
                     $data = $ID;
                     $ID = $storedID;
                 } else {
                     $storedID = $ID;
                 }
                 $data = $this->_addPrePostFixes($column['type'], $data);
                 // Trim document_{start,end}, p_{open,close} from instructions
                 $allinstructions = p_get_instructions($data);
                 $wraps = 1;
                 if (isset($allinstructions[1]) && $allinstructions[1][0] == 'p_open') {
                     $wraps++;
                 }
                 $instructions = array_slice($allinstructions, $wraps, -$wraps);
                 $outs[] = p_render($R->getFormat(), $instructions, $byref_ignore);
                 $ID = $oldid;
                 break;
             default:
                 $val = $this->_addPrePostFixes($column['type'], $val);
                 //type '_img' or '_img<width>'
                 if (substr($type, 0, 3) == 'img') {
                     $width = (int) substr($type, 3);
                     if (!$width) {
                         $width = $this->getConf('image_width');
                     }
                     list($mediaid, $title) = explode('|', $val, 2);
                     if ($title === null) {
                         $title = $column['key'] . ': ' . basename(str_replace(':', '/', $mediaid));
                     } else {
                         $title = trim($title);
                     }
                     if (media_isexternal($val)) {
                         $html = $R->externalmedia($mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true);
                     } else {
                         $html = $R->internalmedia($mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true);
                     }
                     if (strpos($html, 'mediafile') === false) {
                         $html = str_replace('href', 'rel="lightbox" href', $html);
                     }
                     $outs[] = $html;
                 } else {
                     $outs[] = hsc($val);
                 }
         }
     }
     return join(', ', $outs);
 }