fmsystem-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Fmsystem-commits] [6643] property: Fileuploader


From: Sigurd Nes
Subject: [Fmsystem-commits] [6643] property: Fileuploader
Date: Thu, 25 Nov 2010 22:50:42 +0000

Revision: 6643
          http://svn.sv.gnu.org/viewvc/?view=rev&root=fmsystem&revision=6643
Author:   sigurdne
Date:     2010-11-25 22:50:42 +0000 (Thu, 25 Nov 2010)
Log Message:
-----------
property: Fileuploader

Modified Paths:
--------------
    trunk/property/inc/class.fileuploader.inc.php
    trunk/property/inc/class.uientity.inc.php
    trunk/property/js/yahoo/entity.edit.js

Modified: trunk/property/inc/class.fileuploader.inc.php
===================================================================
--- trunk/property/inc/class.fileuploader.inc.php       2010-11-25 17:12:15 UTC 
(rev 6642)
+++ trunk/property/inc/class.fileuploader.inc.php       2010-11-25 22:50:42 UTC 
(rev 6643)
@@ -24,7 +24,7 @@
        * @internal Development of this application was funded by 
http://www.bergen.kommune.no/bbb_/ekstern/
        * @package property
        * @subpackage location
-       * @version $Id: class.uilocation.inc.php 5083 2010-03-19 14:29:26Z 
sigurd $
+       * @version $Id: class.fileuploader.inc.php 5083 2010-03-19 14:29:26Z 
sigurd $
        */
 
        /**
@@ -43,7 +43,7 @@
 
                function __construct()
                {
-                       $GLOBALS['phpgw_info']['flags']['xslt_app']             
        = true;
+                       $GLOBALS['phpgw_info']['flags']['xslt_app']             
        = false;
                        $GLOBALS['phpgw_info']['flags']['noframework']          
= true;
                        $GLOBALS['phpgw_info']['flags']['no_reset_fonts']       
= true;
                }
@@ -63,6 +63,15 @@
                                'domain'                                => 
phpgw::get_var('domain')
                        );
                                
+                       $oArgs = "{menuaction:'$upload_target',"
+                               ."id:'$id',"
+                               ."last_loginid:'". 
phpgw::get_var('last_loginid')."',"
+                               ."last_domain:'" . 
phpgw::get_var('last_domain')."',"
+                               ."sessionphpgwsessid:'" . 
phpgw::get_var('sessionphpgwsessid')."',"
+                               ."domain:'" . phpgw::get_var('domain')."'}";
+
+
+
                        foreach ($_GET as $varname => $value)
                        {
                                if(strpos($varname, '_')===0)
@@ -73,8 +82,48 @@
 
                        $upload_url     = $GLOBALS['phpgw']->link('/index.php', 
$link_data);
 
-                       $js_code = self::get_js($upload_url);
-               
+                       $js_code = self::get_js($oArgs);
+
+                       $title = lang('fileuploader');
+                       $html = <<<HTML
+                       <!DOCTYPE html>
+                       <html>
+                               <head>
+                                       <title>{$title}</title>
+                                       <link 
href="{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/swfupload/default.css"
 rel="stylesheet" type="text/css" />
+                                       <script type="text/javascript" 
src="{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/core/base.js"></script>
+                                       <script type="text/javascript" 
src="{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/swfupload/swfupload.js"></script>
+                                       <script type="text/javascript" 
src="{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/swfupload/swfupload.queue.js"></script>
+                                       <script type="text/javascript" 
src="{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/swfupload/fileprogress.js"></script>
+                                       <script type="text/javascript" 
src="{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/swfupload/handlers.js"></script>
+                                       $js_code
+                               </head>
+                               <body>
+
+                               <div id="content">
+                                       <h2>{$title}</h2>
+                                       <form id="form1" action="index.php" 
method="post" enctype="multipart/form-data">
+
+                                                       <div class="fieldset 
flash" id="fsUploadProgress">
+                                                       <span 
class="legend">Upload Queue</span>
+                                                       </div>
+                                               <div id="divStatus">0 Files 
Uploaded</div>
+                                                       <div>
+                                                               <span 
id="spanButtonPlaceHolder"></span>
+                                                               <input 
id="btnCancel" type="button" value="Cancel All Uploads" 
onclick="swfu.cancelQueue();" disabled="disabled" style="margin-left: 2px; 
font-size: 8pt; height: 29px;" />
+                                                       </div>
+                                       </form>
+                               </div>
+                       </body>
+               </html>
+HTML;
+
+
+
+
+                       echo $html;
+
+/*             
                        
$GLOBALS['phpgw']->css->add_external_file('phpgwapi/js/yahoo/datatable/assets/skins/sam/datatable.css');
                        
$GLOBALS['phpgw']->css->add_external_file('phpgwapi/js/yahoo/fonts/fonts-min.css');
                        phpgwapi_yui::load_widget('uploader');
@@ -85,175 +134,250 @@
                                'js_code' => $js_code,
                        );
                        $GLOBALS['phpgw']->xslttpl->set_var('phpgw', 
array('fileuploader' => $data));
+*/
                }
 
                
-               static function get_js($upload_url = '')
+               static function get_js($oArgs = '')
                {
+                       $button_text = lang('Select Files');
+                       $str_base_url = $GLOBALS['phpgw']->link('/', array(), 
true);
                        $js_code = <<<JS
-                       YAHOO.util.Event.onDOMReady(function () { 
-                       var uiLayer = YAHOO.util.Dom.getRegion('selectLink');
-                       var overlay = YAHOO.util.Dom.get('uploaderOverlay');
-                       YAHOO.util.Dom.setStyle(overlay, 'width', 
uiLayer.right-uiLayer.left + "px");
-                       YAHOO.util.Dom.setStyle(overlay, 'height', 
uiLayer.bottom-uiLayer.top + "px");
-                       });
+<script type="text/javascript">
+               var swfu;
+               var strBaseURL = '$str_base_url';
 
-                       // Custom URL for the uploader swf file (same folder).
+               var sUrl = phpGWLink('index.php', $oArgs);
 
-                       YAHOO.widget.Uploader.SWFURL = 
"{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/yahoo/uploader/assets/uploader.swf";
 
-                       // Instantiate the uploader and write it to its 
placeholder div.
-                       var uploader = new YAHOO.widget.Uploader( 
"uploaderOverlay" );
-                       
-                       // Add event listeners to various events on the 
uploader.
-                       // Methods on the uploader should only be called once 
the 
-                       // contentReady event has fired.
+               window.onload = function() {
+                       var settings = {
+                               flash_url : 
"{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/swfupload/swfupload.swf",
+                               flash9_url : 
"{$GLOBALS['phpgw_info']['server']['webserver_url']}/phpgwapi/js/swfupload/swfupload_fp9.swf",
+                               upload_url: sUrl,
+//                             post_params: {"PHPSESSID" : "<?php echo 
session_id(); ?>"},
+                               file_size_limit : "100 MB",
+                               file_types : "*.*",
+                               file_types_description : "All Files",
+                               file_upload_limit : 100,
+                               file_queue_limit : 0,
+                               custom_settings : {
+                                       progressTarget : "fsUploadProgress",
+                                       cancelButtonId : "btnCancel"
+                               },
+                               debug: false,
+
+                               // Button settings
+                               button_image_url: 
"images/TestImageNoText_65x29.png",
+                               button_width: "65",
+                               button_height: "29",
+                               button_placeholder_id: "spanButtonPlaceHolder",
+                               button_text: '<span 
class="theFont">{$button_text}</span>',
+                               button_text_style: ".theFont { font-size: 16; 
}",
+                               button_text_left_padding: 12,
+                               button_text_top_padding: 3,
+                               
+                               // The event handler functions are defined in 
handlers.js
+                               swfupload_preload_handler : preLoad,
+                               swfupload_load_failed_handler : loadFailed,
+                               file_queued_handler : fileQueued,
+                               file_queue_error_handler : fileQueueError,
+                               file_dialog_complete_handler : 
fileDialogComplete,
+                               upload_start_handler : uploadStart,
+                               upload_progress_handler : uploadProgress,
+                               upload_error_handler : uploadError,
+                               upload_success_handler : uploadSuccess,
+                               upload_complete_handler : uploadComplete,
+                               queue_complete_handler : queueComplete  // 
Queue plugin event
+                       };
+
+                       swfu = new SWFUpload(settings);
+            };
+       </script>
+JS;
+                       return $js_code;
+               }
+
+
+/*
+This is an upload script for SWFUpload that attempts to properly handle 
uploaded files
+in a secure way.
+
+Notes:
        
-                       uploader.addListener('contentReady', 
handleContentReady);
-                       uploader.addListener('fileSelect', onFileSelect)
-                       uploader.addListener('uploadStart', onUploadStart);
-                       uploader.addListener('uploadProgress', 
onUploadProgress);
-                       uploader.addListener('uploadCancel', onUploadCancel);
-                       uploader.addListener('uploadComplete', 
onUploadComplete);
-                       uploader.addListener('uploadCompleteData', 
onUploadResponse);
-                       uploader.addListener('uploadError', onUploadError);
-                       uploader.addListener('rollOver', handleRollOver);
-                       uploader.addListener('rollOut', handleRollOut);
-                       uploader.addListener('click', handleClick);
-       
-                       // Variable for holding the filelist.
-                       var fileList;
+       SWFUpload doesn't send a MIME-TYPE. In my opinion this is ok since 
MIME-TYPE is no better than
+        file extension and is probably worse because it can vary from OS to OS 
and browser to browser (for the same file).
+        The best thing to do is content sniff the file but this can be 
resource intensive, is difficult, and can still be fooled or inaccurate.
+        Accepting uploads can never be 100% secure.
+        
+       You can't guarantee that SWFUpload is really the source of the upload.  
A malicious user
+        will probably be uploading from a tool that sends invalid or false 
metadata about the file.
+        The script should properly handle this.
+        
+       The script should not over-write existing files.
        
-                       // When the mouse rolls over the uploader, this function
-                       // is called in response to the rollOver event.
-                       // It changes the appearance of the UI element below 
the Flash overlay.
-                       function handleRollOver () {
-                               
YAHOO.util.Dom.setStyle(YAHOO.util.Dom.get('selectLink'), 'color', "#FFFFFF");
-                               
YAHOO.util.Dom.setStyle(YAHOO.util.Dom.get('selectLink'), 'background-color', 
"#000000");
-                       }
+       The script should strip away invalid characters from the file name or 
reject the file.
+       
+       The script should not allow files to be saved that could then be 
executed on the webserver (such as .php files).
+        To keep things simple we will use an extension whitelist for allowed 
file extensions.  Which files should be allowed
+        depends on your server configuration. The extension white-list is 
_not_ tied your SWFUpload file_types setting
+       
+       For better security uploaded files should be stored outside the 
webserver's document root.  Downloaded files
+        should be accessed via a download script that proxies from the file 
system to the webserver.  This prevents
+        users from executing malicious uploaded files.  It also gives the 
developer control over the outgoing mime-type,
+        access restrictions, etc.  This, however, is outside the scope of this 
script.
+       
+       SWFUpload sends each file as a separate POST rather than several files 
in a single post. This is a better
+        method in my opinion since it better handles file size limits, e.g., 
if post_max_size is 100 MB and I post two 60 MB files then
+        the post would fail (2x60MB = 120MB). In SWFupload each 60 MB is 
posted as separate post and we stay within the limits. This
+        also simplifies the upload script since we only have to handle a 
single file.
+       
+       The script should properly handle situations where the post was too 
large or the posted file is larger than
+        our defined max.  These values are not tied to your SWFUpload 
file_size_limit setting.
+       
+*/
 
-                       // On rollOut event, this function is called, which 
changes the appearance of the
-                       // UI element below the Flash layer back to its 
original state.
-                       function handleRollOut () {
-                               
YAHOO.util.Dom.setStyle(YAHOO.util.Dom.get('selectLink'), 'color', "#0000CC");
-                               
YAHOO.util.Dom.setStyle(YAHOO.util.Dom.get('selectLink'), 'background-color', 
"#FFFFFF");
-                       }
+               function upload()
+               {
+                       // Check post_max_size 
(http://us3.php.net/manual/en/features.file-upload.php#73762)
+                       $POST_MAX_SIZE = ini_get('post_max_size');
+                       $unit = strtoupper(substr($POST_MAX_SIZE, -1));
+                       $multiplier = ($unit == 'M' ? 1048576 : ($unit == 'K' ? 
1024 : ($unit == 'G' ? 1073741824 : 1)));
 
-                       // When the Flash layer is clicked, the "Browse" dialog 
is invoked.
-                       // The click event handler allows you to do something 
else if you need to.
-                       function handleClick () {
+                       if ((int)$_SERVER['CONTENT_LENGTH'] > 
$multiplier*(int)$POST_MAX_SIZE && $POST_MAX_SIZE)
+                       {
+                               header("HTTP/1.1 500 Internal Server Error"); 
// This will trigger an uploadError event in SWFUpload
+                               echo "POST exceeded maximum allowed size.";
+                               exit(0);
                        }
 
-                       // When contentReady event is fired, you can call 
methods on the uploader.
-                       function handleContentReady () {
-                           // Allows the uploader to send log messages to 
trace, as well as to YAHOO.log
-                               uploader.setAllowLogging(true);
+               // Settings
+                       $save_path = dirname(__FILENAME__) . "/uploads/";       
// The path were we will save the file (getcwd() may not be reliable and should 
be tested in your environment)
+                       $save_path = 
"{$GLOBALS['phpgw_info']['server']['temp_dir']}/";
+                       $upload_name = "Filedata";
+                       $max_file_size_in_bytes = 2147483647;                   
        // 2GB in bytes
+                       $extension_whitelist = array("jpg", "gif", "png");      
// Allowed file extensions
+                       $valid_chars_regex = '.A-Z0-9_ 
address@hidden&()+={}\[\]\',~`-';                                // Characters 
allowed in the file name (in a Regular Expression format)
+               
+                       // Other variables      
+                       $MAX_FILENAME_LENGTH = 260;
+                       $file_name = "";
+                       $file_extension = "";
+                       $uploadErrors = array
+                       (
+                       0=>"There is no error, the file uploaded successfully",
+                       1=>"The uploaded file exceeds the upload_max_filesize 
directive in php.ini",
+                       2=>"The uploaded file exceeds the MAX_FILE_SIZE 
directive that was specified in the HTML form",
+                       3=>"The uploaded file was only partially uploaded",
+                       4=>"No file was uploaded",
+                       6=>"Missing a temporary folder"
+                       );
 
-                               // Allows multiple file selection in "Browse" 
dialog.
-                               uploader.setAllowMultipleFiles(true);
 
-                               // New set of file filters.
-                               var ff = new Array({description:"Images", 
extensions:"*.jpg;*.png;*.gif"},
-                                                  {description:"Videos", 
extensions:"*.avi;*.mov;*.mpg"});
-
-                               // Apply new set of file filters to the 
uploader.
-//                             uploader.setFileFilters(ff);
+               // Validate the upload
+                       if (!isset($_FILES[$upload_name]))
+                       {
+                               $this->HandleError("No upload found in \$_FILES 
for " . $upload_name);
+                               exit(0);
                        }
-
-                       // Actually uploads the files. In this case,
-                       // uploadAll() is used for automated queueing and 
upload 
-                       // of all files on the list.
-                       // You can manage the queue on your own and use 
"upload" instead,
-                       // if you need to modify the properties of the request 
for each
-                       // individual file.
-                       function upload() {
-                               if (fileList != null) {
-//                                     
uploader.setSimUploadLimit(parseInt(document.getElementById("simulUploads").value));
-                                       uploader.setSimUploadLimit(1);
-                                       uploader.uploadAll("{$upload_url}", 
"POST", null, "Filedata");
-                               }       
+                       else if (isset($_FILES[$upload_name]["error"]) && 
$_FILES[$upload_name]["error"] != 0)
+                       {
+                               
$this->HandleError($uploadErrors[$_FILES[$upload_name]["error"]]);
+                               exit(0);
                        }
+                       else if (!isset($_FILES[$upload_name]["tmp_name"]) || 
address@hidden($_FILES[$upload_name]["tmp_name"]))
+                       {
+                               $this->HandleError("Upload failed 
is_uploaded_file test.");
+                               exit(0);
+                       }
+                       else if (!isset($_FILES[$upload_name]['name']))
+                       {
+                               $this->HandleError("File has no name.");
+                               exit(0);
+                       }
+       
+               // Validate the file size (Warning: the largest files supported 
by this code is 2GB)
+                       $file_size = 
@filesize($_FILES[$upload_name]["tmp_name"]);
+                       if (!$file_size || $file_size > $max_file_size_in_bytes)
+                       {
+                               $this->HandleError("File exceeds the maximum 
allowed size");
+                               exit(0);
+                       }
+       
+                       if ($file_size <= 0)
+                       {
+                               $this->HandleError("File size outside allowed 
lower bound");
+                               exit(0);
+                       }
 
-                       // Fired when the user selects files in the "Browse" 
dialog
-                       // and clicks "Ok".
-                       function onFileSelect(event) {
-                               if('fileList' in event && event.fileList != 
null) {
-                                       fileList = event.fileList;
-                                       createDataTable(fileList);
-                               }
+               // Validate file name (for our purposes we'll just remove 
invalid characters)
+                       $file_name = 
preg_replace('/[^'.$valid_chars_regex.']|\.+$/i', "", 
basename($_FILES[$upload_name]['name']));
+                       if (strlen($file_name) == 0 || strlen($file_name) > 
$MAX_FILENAME_LENGTH)
+                       {
+                               $this->HandleError("Invalid file name");
+                               exit(0);
                        }
 
-                       function createDataTable(entries) {
-                         rowCounter = 0;
-                         this.fileIdHash = {};
-                         this.dataArr = [];
-                         for(var i in entries) {
-                            var entry = entries[i];
-                                entry["progress"] = "<div 
style='height:5px;width:100px;background-color:#CCC;'></div>";
-                            dataArr.unshift(entry);
-                         }
 
-                         for (var j = 0; j < dataArr.length; j++) {
-                           this.fileIdHash[dataArr[j].id] = j;
-                         }
-
-                           var myColumnDefs = [
-                               {key:"name", label: "File Name", 
sortable:false},
-                               {key:"size", label: "Size", sortable:false},
-                               {key:"progress", label: "Upload progress", 
sortable:false}
-                           ];
-
-                       this.myDataSource = new YAHOO.util.DataSource(dataArr);
-                       this.myDataSource.responseType = 
YAHOO.util.DataSource.TYPE_JSARRAY;
-                       this.myDataSource.responseSchema = {
-                       fields: ["id","name","created","modified","type", 
"size", "progress"]
-               };
-
-                       this.singleSelectDataTable = new 
YAHOO.widget.DataTable("dataTableContainer",
-                                  myColumnDefs, this.myDataSource, {
-                                      caption:"Files To Upload",
-                                      selectionMode:"single"
-                                  });
+               // Validate that we won't over-write an existing file
+                       if (file_exists($save_path . $file_name))
+                       {
+                               $this->HandleError("File with this name already 
exists");
+                               exit(0);
                        }
 
-                       // Do something on each file's upload start.
-                       function onUploadStart(event) {
-                       
+               // Validate file extension
+                       $path_info = pathinfo($_FILES[$upload_name]['name']);
+                       $file_extension = $path_info["extension"];
+                       $is_valid_extension = false;
+                       foreach ($extension_whitelist as $extension)
+                       {
+                               if (strcasecmp($file_extension, $extension) == 
0)
+                               {
+                                       $is_valid_extension = true;
+                                       break;
+                               }
                        }
-
-                       // Do something on each file's upload progress event.
-                       function onUploadProgress(event) {
-                               rowNum = fileIdHash[event["id"]];
-                               prog = 
Math.round(100*(event["bytesLoaded"]/event["bytesTotal"]));
-                               progbar = "<div 
style='height:5px;width:100px;background-color:#CCC;'><div 
style='height:5px;background-color:#F00;width:" + prog + "px;'></div></div>";
-                               singleSelectDataTable.updateRow(rowNum, {name: 
dataArr[rowNum]["name"], size: dataArr[rowNum]["size"], progress: progbar});    
 
+                       if (!$is_valid_extension)
+                       {
+                               $this->HandleError("Invalid file extension");
+                               exit(0);
                        }
 
-                       // Do something when each file's upload is complete.
-                       function onUploadComplete(event) {
-                               rowNum = fileIdHash[event["id"]];
-                               prog = 
Math.round(100*(event["bytesLoaded"]/event["bytesTotal"]));
-                               progbar = "<div 
style='height:5px;width:100px;background-color:#CCC;'><div 
style='height:5px;background-color:#F00;width:100px;'></div></div>";
-                               singleSelectDataTable.updateRow(rowNum, {name: 
dataArr[rowNum]["name"], size: dataArr[rowNum]["size"], progress: progbar});
-                       }
+               // Validate file contents (extension and mime-type can't be 
trusted)
+                       /*
+                               Validating the file contents is OS and web 
server configuration dependant.  Also, it may not be reliable.
+                               See the comments on this page: 
http://us2.php.net/fileinfo
+               
+                               Also see 
http://72.14.253.104/search?q=cache:3YGZfcnKDrYJ:www.scanit.be/uploads/php-file-upload.pdf+php+file+command&hl=en&ct=clnk&cd=8&gl=us&client=firefox-a
+                                which describes how a PHP script can be 
embedded within a GIF image file.
+               
+                               Therefore, no sample code will be provided 
here.  Research the issue, decide how much security is
+                               needed, and implement a solution that meets the 
need.
+                       */
 
-                       // Do something if a file upload throws an error.
-                       // (When uploadAll() is used, the Uploader will
-                       // attempt to continue uploading.
-                       function onUploadError(event) {
-                               console.log(event);
-                       }
 
-                       // Do something if an upload is cancelled.
-                       function onUploadCancel(event) {
-
+               // Process the file
+                       /*
+                               At this point we are ready to process the valid 
file. This sample code shows how to save the file. Other tasks
+                                could be done such as creating an entry in a 
database or generating a thumbnail.
+                
+                               Depending on your server OS and needs you may 
need to set the Security Permissions on the file after it has
+                               been saved.
+                       */
+                       if (address@hidden($_FILES[$upload_name]["tmp_name"], 
$save_path.$file_name))
+                       {
+                               $this->HandleError("File could not be saved.");
+                               exit(0);
                        }
 
-                       // Do something when data is received back from the 
server.
-                       function onUploadResponse(event) {
+                       exit(0);
+               }
 
-                       }
-JS;
-                       return $js_code;
+               /* Handles the error output. This error message will be sent to 
the uploadSuccess event handler.  The event handler
+               will have to check for any error messages and react as needed. 
*/
+               function HandleError($message)
+               {
+                       echo $message;
                }
  }

Modified: trunk/property/inc/class.uientity.inc.php
===================================================================
--- trunk/property/inc/class.uientity.inc.php   2010-11-25 17:12:15 UTC (rev 
6642)
+++ trunk/property/inc/class.uientity.inc.php   2010-11-25 22:50:42 UTC (rev 
6643)
@@ -158,6 +158,9 @@
                        $id                             = phpgw::get_var('id', 
'int');
                        $jasperfile             = phpgw::get_var('jasperfile', 
'bool');
 
+                       $fileuploader   = CreateObject('property.fileuploader');
+                       $fileuploader->upload();
+
                        if(!$this->acl_add && !$this->acl_edit)
                        {
                                $GLOBALS['phpgw']->common->phpgw_exit();

Modified: trunk/property/js/yahoo/entity.edit.js
===================================================================
--- trunk/property/js/yahoo/entity.edit.js      2010-11-25 17:12:15 UTC (rev 
6642)
+++ trunk/property/js/yahoo/entity.edit.js      2010-11-25 22:50:42 UTC (rev 
6643)
@@ -23,7 +23,7 @@
                        var frame = document.createElement('iframe');
                        frame.src = sUrl;
                        frame.width = "100%";
-                       frame.height = "350";
+                       frame.height = "600";
                        o.setBody(frame);
                };
                lightbox.showEvent.subscribe(onDialogShow, lightbox);




reply via email to

[Prev in Thread] Current Thread [Next in Thread]