4 * Utility classes to manage uploaded files
6 * Copyright (c) 2011 Olivier Berger & Institut Telecom
8 * This program was developped in the frame of the COCLICO project
9 * (http://www.coclico-project.org/) with financial support of the Paris
12 * This file is part of FusionForge. FusionForge is free software;
13 * you can redistribute it and/or modify it under the terms of the
14 * GNU General Public License as published by the Free Software
15 * Foundation; either version 2 of the Licence, or (at your option)
18 * FusionForge is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
23 * You should have received a copy of the GNU General Public License along
24 * with FusionForge; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
28 // TODO : add cleanup mechanism for storage
32 * Generic file storage management class
33 * The files are stored in an arbitrary dir on the server side
35 * @author Olivier Berger
38 class AbstractFilesDirectory extends Error {
42 protected $html_generator;
44 protected static $finfo;
47 * Initializes the directory permissions
49 protected function createStorage() {
50 // initialize the group storage dir
51 if (!is_dir($this->dir_path)) {
52 mkdir($this->dir_path,0755);
54 if ( fileperms($this->dir_path) != 0x4755 ) {
55 chmod($this->dir_path,0755);
60 protected static function finfo() {
61 if (!isset(self::$finfo)) {
62 self::$finfo = new finfo(FILEINFO_MIME, forge_get_config('libmagic_db', 'projectimport'));
68 * @param HTML generator $HTML
69 * @param string $storage_base path to the storage directory (if omitted, uses a temprary dir in /tmp)
71 public function AbstractFilesDirectory($HTML, $storage_base=False) {
73 $this->html_generator = $HTML;
75 if (!isset(self::$finfo)) {
76 self::$finfo = new finfo(FILEINFO_MIME, forge_get_config('libmagic_db', 'projectimport'));
80 $storage_base = tempnam("/tmp", "ff-projectimport");
82 $this->dir_path = $storage_base;
83 $this->createStorage();
87 * Move a file on the server (typically an uploaded one) into the storage dir
88 * @param string $filename
89 * @param string $newfilename (optional) rename file on the fly
90 * @return boolean|string
92 public function addFileMovingIt($filename, $newfilename=FALSE) {
94 $newpath = $this->dir_path . $newfilename;
96 $newpath = $this->dir_path . basename($filename);
98 // do the move of the file
99 $ret = rename($filename, $newpath);
101 $this->setError(sprintf(_('File %s cannot be moved to the storage location %s'), $filename, $newpath));
108 * Displays an HTML list of the directory contents
109 * @return string HTML
111 public function displayContents() {
113 if (is_dir($this->dir_path)) {
114 // maybe use scandir instead ?
115 if ($dh = opendir($this->dir_path)) {
118 while (($file = readdir($dh)) !== false) {
119 if ($file != '.' && $file != '..') {
120 $html.='<li>'.'filename: '. $file .': filetype: ' . filetype($this->dir_path . $file) . '</li>';
130 public function getMimeType($filepath) {
131 $finfo = self::finfo();
133 $this->setError(_('Opening fileinfo database failed'));
136 $mimetype = $finfo->file($filepath);
137 $mimetype = strstr($mimetype, ';', true);
139 // try to identify OpenDocument Package Zip container
140 if ($mimetype == 'application/octet-stream') {
141 $zip = new ZipArchive;
142 $res = $zip->open($filepath);
144 // if it has a mimetype file in the zip, then read its contents
145 $contents = $zip->getFromName('mimetype');
147 $mimetype = $contents;
155 * Returns an HTML box/table containing (single) file selection radio buttons
156 * @param string $preselected filename
157 * @return boolean|string
159 public function displayFileSelectionForm($preselected = False) {
164 if (is_dir($this->dir_path)) {
165 $contents = scandir($this->dir_path);
166 if(count($contents) > 2) {
167 $html .= $this->html_generator->boxTop(_("Uploaded files available"));
168 $html .= '<table width="100%"><thead><tr>';
169 $html .= '<th>'._('name').'</th>';
170 $html .= '<th>'._('type').'</th>';
171 $html .= '<th>'._('selected ?').'</th>';
172 $html .= '</tr></thead><tbody>';
174 foreach($contents as $file) {
175 if ($file != '.' && $file != '..') {
176 $filepath = $this->dir_path . $file;
177 $filetype = filetype($filepath);
179 if ($filetype == 'file') {
180 $mimetype = $this->getMimeType($filepath);
183 if ($mimetype == 'application/x-planetforge-forge-export') {
184 $html .= '<td style="white-space: nowrap;"><tt><b>'. $file .'</b></tt></td>';
185 $mimetype = '<b>PlanetForge forge export container ('.$mimetype.')</b>';
188 $html .= '<td style="white-space: nowrap;"><tt>'. $file .'</tt></td>';
190 if ($filetype == 'file') {
191 $html .= '<td style="white-space: nowrap;">' . $mimetype . '</td>';
192 $sha_filename = sha1($file);
193 if ($file == $preselected) {
194 $html .= '<td><input type="radio" name="file_'.$sha_filename.'" value="'.$sha_filename.'" checked="checked" /></td>';
196 $html .= '<td><input type="radio" $group = group_get_object($group_id);
197 name="file_'.$sha_filename.'" value="'.$sha_filename.'" /></td>';
201 $html .= '<td style="white-space: nowrap;">' . $filetype . '</td>';
202 $html .= '<td style="white-space: nowrap;" />';
206 $html .= '<input type="hidden" name="submit_file" value="y" />';
207 $html .= '</tbody></table>';
208 $html .= $this->html_generator->boxBottom();
216 * Returns the path given a SHA1 hash for a filename
217 * @param unknown_type $filesha1
218 * @return Ambigous <boolean, string>
220 public function getFilePath($filesha1) {
222 if (is_dir($this->dir_path)) {
223 $contents = scandir($this->dir_path);
224 foreach($contents as $file) {
225 if ($filesha1 == sha1($file)) {
226 $filepath = $this->dir_path . $file;
237 * Specialized file storage management class for site-level files
239 * Files are stored inside $projectimport/storage_base (for instance '$core/data_path/plugins/projectimport/)
241 * @author Olivier Berger
244 class SiteAdminFilesDirectory extends AbstractFilesDirectory {
245 public function SiteAdminFilesDirectory($HTML) {
247 $storage_base = forge_get_config('storage_base', 'projectimport');
249 parent::AbstractFilesDirectory($HTML, $storage_base);
255 * Specialized file storage management class for project-level files
257 * Files are stored inside subdirs of $projectimport/storage_base (for instance '$core/data_path/plugins/projectimport/_projname_/)
259 * @author Olivier Berger
262 class ProjectFilesDirectory extends AbstractFilesDirectory {
266 * @param HTML generator $HTML
267 * @param integer $group_id
269 public function ProjectFilesDirectory($HTML, $group_id) {
271 // store the project files inside a group unix name's subdir
272 $group = group_get_object($group_id);
273 $storage_base = forge_get_config('storage_base', 'projectimport');
274 $storage_base .= '/'. $group->getUnixName().'/';
276 parent::AbstractFilesDirectory($HTML, $storage_base);
283 * Utility HTML display class for pages containing a file upload and selection form
285 * @author Olivier Berger
288 class FileManagerPage {
291 * filename of the selected file POSTed
294 protected $posted_selecteddumpfile;
296 * filename of the uploaded file POSTed
299 protected $posted_uploadedfile;
301 protected $html_generator;
307 * @var AbstractFilesDirectory
313 * @param HTML generator $HTML
314 * @param AbstractFilesDirectory $storage (optional)
316 function FileManagerPage($HTML, $storage=False) {
317 $this->html_generator = $HTML;
320 // If specialized storage provided, then use it
322 $this->storage = $storage;
324 // otherwise create one with temporary directory
325 $this->storage = new AbstractFilesDirectory($this->html_generator);
327 $this->posted_selecteddumpfile = False;
328 $this->posted_uploadedfile = False;
332 * Adds a $feedback message
333 * @param string $message
335 function feedback($message) {
337 if ($feedback) $feedback .= '<br />';
338 $feedback .= $message;
342 * Parses the POSTed data to initialize the $posted_selecteddumpfile and $posted_uploadedfile and returns selected file name (if any)
343 * @return Ambigous <boolean, Ambigous, string>
345 function initialize_chosenfile_from_submitted() {
349 $uploaded_file = getUploadedFile('uploaded_file');
350 //print_r($uploaded_file);
352 // process chosen file -> $filechosen set after this (or not)
353 if (getStringFromPost('submit_file')) {
354 $filesha1s = array();
355 foreach (array_keys($_POST) as $key) {
356 if(!strncmp($key, 'file_', 5)) {
357 $filesha1 = substr($key, 5);
358 $filesha1s[] = $filesha1;
361 if (count($filesha1s) > 1) {
363 $this->feedback(_('Please select only one file'));
365 if (count($filesha1s) == 1) {
366 $filechosen = $this->storage->getFilePath($filesha1s[0]);
368 $this->feedback(_('File not found on server'));
374 // Process uploaded file : $this->posted_selecteddumpfile set afterwards (or not)
376 // May use codendi's rules to check results of upload ?
377 //$rule_file = new Rule_File();
378 //if ($rule_file->isValid($uploaded_file)) {
379 if($uploaded_file['error'] == UPLOAD_ERR_OK ) {
381 $this->feedback(_('Please either select existing file OR upload new file'));
385 $imported_file = $uploaded_file['tmp_name'];
386 $imported_file = $this->storage->addFileMovingIt($imported_file, $uploaded_file['name']);
387 if(! $imported_file) {
388 $this->feedback($this->storage->getErrorMessage());
391 $this->posted_uploadedfile = $uploaded_file['name'];
392 $this->message .= sprintf(_('File "%s" uploaded and pre-selected'),$this->posted_uploadedfile);
397 $error_code = $uploaded_file['error'];
398 if ($error_code != UPLOAD_ERR_NO_FILE ) {
399 switch ($error_code) {
400 case UPLOAD_ERR_INI_SIZE:
401 $this->feedback(_('The uploaded file exceeds the upload_max_filesize directive in php.ini'));
402 case UPLOAD_ERR_FORM_SIZE:
403 $this->feedback(_('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'));
404 case UPLOAD_ERR_PARTIAL:
405 $this->feedback(_('The uploaded file was only partially uploaded'));
406 /* case UPLOAD_ERR_NO_FILE:
407 return 'No file was uploaded';*/
408 case UPLOAD_ERR_NO_TMP_DIR:
409 $this->feedback(_('Missing a temporary folder'));
410 case UPLOAD_ERR_CANT_WRITE:
411 $this->feedback(_('Failed to write file to disk'));
412 case UPLOAD_ERR_EXTENSION:
413 $this->feedback(_('File upload stopped by extension'));
415 $this->feedback(_('Unknown upload error %d', $error_code));
421 $this->posted_selecteddumpfile = $filechosen;