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
31 * Generic file storage management class
32 * The files are stored in an arbitrary dir on the server side
34 * @author Olivier Berger
37 class AbstractFilesDirectory extends FFError {
41 protected $html_generator;
43 protected static $finfo;
46 * Initializes the directory permissions
48 protected function createStorage() {
49 // initialize the group storage dir
50 if (!is_dir($this->dir_path)) {
51 mkdir($this->dir_path,0755);
53 if ( fileperms($this->dir_path) != 0x4755 ) {
54 chmod($this->dir_path,0755);
59 protected static function finfo() {
60 if (!isset(self::$finfo)) {
61 self::$finfo = new finfo(FILEINFO_MIME, forge_get_config('libmagic_db', 'projectimport'));
66 * @param HTML generator $HTML
67 * @param string $storage_base path to the storage directory (if omitted, uses a temprary dir in /tmp)
69 function __construct($HTML, $storage_base=False) {
71 $this->html_generator = $HTML;
73 if (!isset(self::$finfo)) {
74 self::$finfo = new finfo(FILEINFO_MIME, forge_get_config('libmagic_db', 'projectimport'));
78 $storage_base = tempnam("/tmp", "ff-projectimport");
80 $this->dir_path = $storage_base;
81 $this->createStorage();
85 * Move a file on the server (typically an uploaded one) into the storage dir
86 * @param string $filename
87 * @param string $newfilename (optional) rename file on the fly
88 * @return boolean|string
90 public function addFileMovingIt($filename, $newfilename=FALSE) {
92 $newpath = $this->dir_path . $newfilename;
94 $newpath = $this->dir_path . basename($filename);
96 // do the move of the file
97 $ret = rename($filename, $newpath);
99 $this->setError(sprintf(_('File %s cannot be moved to the storage location %s'), $filename, $newpath));
106 * Displays an HTML list of the directory contents
107 * @return string HTML
109 public function displayContents() {
111 if (is_dir($this->dir_path)) {
112 // maybe use scandir instead ?
113 if ($dh = opendir($this->dir_path)) {
116 while (($file = readdir($dh)) !== false) {
117 if ($file != '.' && $file != '..') {
118 $html.='<li>'.'filename: '. $file .': filetype: ' . filetype($this->dir_path . $file) . '</li>';
128 public function getMimeType($filepath) {
129 $finfo = self::finfo();
131 $this->setError(_('Opening fileinfo database failed'));
134 $mimetype = $finfo->file($filepath);
135 $mimetype = strstr($mimetype, ';', true);
137 // try to identify OpenDocument Package Zip container
138 if ($mimetype == 'application/octet-stream') {
139 $zip = new ZipArchive;
140 $res = $zip->open($filepath);
142 // if it has a mimetype file in the zip, then read its contents
143 $contents = $zip->getFromName('mimetype');
145 $mimetype = $contents;
153 * Returns an HTML box/table containing (single) file selection radio buttons
154 * @param string $preselected filename
155 * @return boolean|string
157 public function displayFileSelectionForm($preselected = False) {
160 if (is_dir($this->dir_path)) {
161 $contents = scandir($this->dir_path);
162 if(count($contents) > 2) {
163 $html .= $this->html_generator->boxTop(_("Uploaded files available"));
164 $html .= '<table width="100%"><thead><tr>';
165 $html .= '<th>'._('name').'</th>';
166 $html .= '<th>'._('type').'</th>';
167 $html .= '<th>'._('selected ?').'</th>';
168 $html .= '</tr></thead><tbody>';
170 foreach($contents as $file) {
171 if ($file != '.' && $file != '..') {
172 $filepath = $this->dir_path . $file;
173 $filetype = filetype($filepath);
175 if ($filetype == 'file') {
176 $mimetype = $this->getMimeType($filepath);
179 if ($mimetype == 'application/x-planetforge-forge-export') {
180 $html .= '<td style="white-space: nowrap;"><tt><b>'. $file .'</b></tt></td>';
181 $mimetype = '<b>PlanetForge forge export container ('.$mimetype.')</b>';
184 $html .= '<td style="white-space: nowrap;"><tt>'. $file .'</tt></td>';
186 if ($filetype == 'file') {
187 $html .= '<td style="white-space: nowrap;">' . $mimetype . '</td>';
188 $sha_filename = sha1($file);
189 if ($file == $preselected) {
190 $html .= '<td><input type="radio" name="file_'.$sha_filename.'" value="'.$sha_filename.'" checked="checked" /></td>';
192 $html .= '<td><input type="radio" $group = group_get_object($group_id);
193 name="file_'.$sha_filename.'" value="'.$sha_filename.'" /></td>';
197 $html .= '<td style="white-space: nowrap;">' . $filetype . '</td>';
198 $html .= '<td style="white-space: nowrap;" />';
202 $html .= '<input type="hidden" name="submit_file" value="y" />';
203 $html .= '</tbody></table>';
204 $html .= $this->html_generator->boxBottom();
212 * Returns the path given a SHA1 hash for a filename
213 * @param unknown_type $filesha1
214 * @return Ambigous <boolean, string>
216 public function getFilePath($filesha1) {
218 if (is_dir($this->dir_path)) {
219 $contents = scandir($this->dir_path);
220 foreach($contents as $file) {
221 if ($filesha1 == sha1($file)) {
222 $filepath = $this->dir_path . $file;
232 * Specialized file storage management class for site-level files
234 * Files are stored inside $projectimport/storage_base (for instance '$core/data_path/plugins/projectimport/)
236 * @author Olivier Berger
239 class SiteAdminFilesDirectory extends AbstractFilesDirectory {
240 public function SiteAdminFilesDirectory($HTML) {
242 $storage_base = forge_get_config('storage_base', 'projectimport');
244 parent::AbstractFilesDirectory($HTML, $storage_base);
250 * Specialized file storage management class for project-level files
252 * Files are stored inside subdirs of $projectimport/storage_base (for instance '$core/data_path/plugins/projectimport/_projname_/)
254 * @author Olivier Berger
257 class ProjectFilesDirectory extends AbstractFilesDirectory {
261 * @param HTML generator $HTML
262 * @param integer $group_id
264 public function ProjectFilesDirectory($HTML, $group_id) {
266 // store the project files inside a group unix name's subdir
267 $group = group_get_object($group_id);
268 $storage_base = forge_get_config('storage_base', 'projectimport');
269 $storage_base .= '/'. $group->getUnixName().'/';
271 parent::AbstractFilesDirectory($HTML, $storage_base);
278 * Utility HTML display class for pages containing a file upload and selection form
280 * @author Olivier Berger
283 class FileManagerPage {
286 * filename of the selected file POSTed
289 protected $posted_selecteddumpfile;
291 * filename of the uploaded file POSTed
294 protected $posted_uploadedfile;
296 protected $html_generator;
302 * @var AbstractFilesDirectory
308 * @param HTML generator $HTML
309 * @param AbstractFilesDirectory $storage (optional)
311 function FileManagerPage($HTML, $storage=False) {
312 $this->html_generator = $HTML;
315 // If specialized storage provided, then use it
317 $this->storage = $storage;
319 // otherwise create one with temporary directory
320 $this->storage = new AbstractFilesDirectory($this->html_generator);
322 $this->posted_selecteddumpfile = False;
323 $this->posted_uploadedfile = False;
327 * Adds a $feedback message
328 * @param string $message
330 function feedback($message) {
332 if ($feedback) $feedback .= '<br />';
333 $feedback .= $message;
337 * Parses the POSTed data to initialize the $posted_selecteddumpfile and $posted_uploadedfile and returns selected file name (if any)
338 * @return Ambigous <boolean, Ambigous, string>
340 function initialize_chosenfile_from_submitted() {
344 $uploaded_file = getUploadedFile('uploaded_file');
345 //print_r($uploaded_file);
347 // process chosen file -> $filechosen set after this (or not)
348 if (getStringFromPost('submit_file')) {
349 $filesha1s = array();
350 foreach (array_keys($_POST) as $key) {
351 if(!strncmp($key, 'file_', 5)) {
352 $filesha1 = substr($key, 5);
353 $filesha1s[] = $filesha1;
356 if (count($filesha1s) > 1) {
358 $this->feedback(_('Please select only one file'));
360 if (count($filesha1s) == 1) {
361 $filechosen = $this->storage->getFilePath($filesha1s[0]);
363 $this->feedback(_('File not found on server'));
369 // Process uploaded file : $this->posted_selecteddumpfile set afterwards (or not)
371 // May use codendi's rules to check results of upload ?
372 //$rule_file = new Rule_File();
373 //if ($rule_file->isValid($uploaded_file)) {
374 if($uploaded_file['error'] == UPLOAD_ERR_OK ) {
376 $this->feedback(_('Please either select existing file OR upload new file'));
380 $imported_file = $uploaded_file['tmp_name'];
381 $imported_file = $this->storage->addFileMovingIt($imported_file, $uploaded_file['name']);
382 if(! $imported_file) {
383 $this->feedback($this->storage->getErrorMessage());
386 $this->posted_uploadedfile = $uploaded_file['name'];
387 $this->message .= sprintf(_('File ā%sā uploaded and pre-selected'),$this->posted_uploadedfile);
392 $error_code = $uploaded_file['error'];
393 if ($error_code != UPLOAD_ERR_NO_FILE ) {
394 switch ($error_code) {
395 case UPLOAD_ERR_INI_SIZE:
396 $this->feedback(_('The uploaded file exceeds the upload_max_filesize directive in php.ini'));
397 case UPLOAD_ERR_FORM_SIZE:
398 $this->feedback(_('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'));
399 case UPLOAD_ERR_PARTIAL:
400 $this->feedback(_('The uploaded file was only partially uploaded.'));
401 /* case UPLOAD_ERR_NO_FILE:
402 return 'No file was uploaded';*/
403 case UPLOAD_ERR_NO_TMP_DIR:
404 $this->feedback(_('Missing a temporary folder.'));
405 case UPLOAD_ERR_CANT_WRITE:
406 $this->feedback(_('Failed to write file to disk.'));
407 case UPLOAD_ERR_EXTENSION:
408 $this->feedback(_('File upload stopped by extension.'));
410 $this->feedback(_('Unknown upload error:'), $error_code);
416 $this->posted_selecteddumpfile = $filechosen;