. * * @package mahara * @subpackage artefact-internal * @author Catalyst IT Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL * @copyright (C) 2006-2008 Catalyst IT Ltd http://catalyst.net.nz * */ defined('INTERNAL') || die(); class PluginArtefactFile extends PluginArtefact { public static function get_artefact_types() { return array( 'file', 'folder', 'image', ); } public static function get_block_types() { return array('image'); } public static function get_plugin_name() { return 'file'; } public static function menu_items() { return array( array( 'path' => 'myportfolio/files', 'url' => 'artefact/file/', 'title' => get_string('myfiles', 'artefact.file'), 'weight' => 20, ), ); } public static function get_event_subscriptions() { $subscriptions = array( (object)array( 'plugin' => 'file', 'event' => 'createuser', 'callfunction' => 'newuser', ), ); return $subscriptions; } public static function postinst() { set_config_plugin('artefact', 'file', 'defaultquota', 10485760); self::resync_filetype_list(); } public static function newuser($event, $user) { if (empty($user->quota)) { update_record('usr', array('quota' => get_config_plugin('artefact', 'file', 'defaultquota')), array('id' => $user['id'])); } } public static function sort_child_data($a, $b) { if ($a->container && !$b->container) { return -1; } else if (!$a->container && $b->container) { return 1; } return strnatcasecmp($a->text, $b->text); } public static function themepaths($type) { static $themepaths = array( 'file' => array( 'images/file.gif', 'images/folder.gif', 'images/image.gif', ), ); return $themepaths[$type]; } public static function jsstrings($type) { static $jsstrings = array( 'file' => array( 'mahara' => array( 'cancel', 'delete', 'edit', 'tags', ), 'artefact.file' => array( 'copyrightnotice', 'create', 'createfolder', 'deletefile?', 'deletefolder?', 'Description', 'destination', 'editfile', 'editfolder', 'File', 'fileexistsoverwritecancel', 'filenamefieldisrequired', 'home', 'Name', 'namefieldisrequired', 'nofilesfound', 'overwrite', 'savechanges', 'timeouterror', 'title', 'titlefieldisrequired', 'unlinkthisfilefromblogposts?', 'upload', 'uploadfile', 'uploadfileexistsoverwritecancel', 'uploadingfiletofolder', 'youmustagreetothecopyrightnotice', ), ), ); return $jsstrings[$type]; } public static function jshelp($type) { static $jshelp = array( 'file' => array( 'artefact.file' => array( 'notice', 'quota_message', 'uploadfile', 'tags', ), ), ); return $jshelp[$type]; } /** * Resyncs the allowed filetypes list with the XML configuration file. * * This can be called on install (and is, in the postinst method above), * and every time an upgrade is made that changes the file. */ function resync_filetype_list() { require_once('xmlize.php'); db_begin(); log_info('Beginning resync of filetype list'); $currentlist = get_column('artefact_file_file_types', 'description'); $newlist = xmlize(file_get_contents(get_config('docroot') . 'artefact/file/filetypes.xml')); $filetypes = $newlist['filetypes']['#']['filetype']; $newfiletypes = array(); // Step one: if a filetype is in the new list that is not in the current // list, add it to the current list. foreach ($filetypes as $filetype) { $type = $filetype['#']['description'][0]['#']; if (!in_array($type, $currentlist)) { log_debug('Adding filetype: ' . $type); $currentlist[] = $type; $record = new StdClass; $record->description = $type; $record->enabled = $filetype['#']['enabled'][0]['#']; insert_record('artefact_file_file_types', $record); } $newfiletypes[] = $type; } // Step two: If a filetype is in the current list that is not in the // new list, remove it from the current list. foreach ($currentlist as $key => $type) { if (!in_array($type, $newfiletypes)) { log_debug('Removing filetype: ' . $type); unset($currentlist[$key]); delete_records('artefact_file_mime_types', 'description', $type); delete_records('artefact_file_file_types', 'description', $type); } } // Get a list of all current mimetypes for each file type $currentmimetypes = array(); $dbmimetypes = get_records_array('artefact_file_mime_types'); if ($dbmimetypes) { foreach ($dbmimetypes as $mimetype) { $currentmimetypes[$mimetype->description][] = $mimetype->mimetype; } } unset($dbmimetypes); // Step three: For each filetype in the current list, update the mime // types allowed for it if necessary foreach ($currentlist as $description) { // Get the new mime types $newmimetypes = array(); foreach ($filetypes as $filetype) { if ($filetype['#']['description'][0]['#'] == $description) { foreach ($filetype['#']['mimetypes'][0]['#']['mimetype'] as $mimetype) { $newmimetypes[] = $mimetype['#']; } } } // Roll up roll up to see the famous array_equals implementation! // You'd think PHP would have a way to do this, but I couldn't find // it... sort($newmimetypes); if (isset($currentmimetypes[$description])) { sort($currentmimetypes[$description]); } if ((!isset($currentmimetypes[$description]) && $newmimetypes) || ((join('', $currentmimetypes[$description]) != join('', $newmimetypes)))) { log_debug('Need to update mime types for ' . $description); delete_records('artefact_file_mime_types', 'description', $description); foreach ($newmimetypes as $newmimetype) { $record = new StdClass; $record->mimetype = $newmimetype; $record->description = $description; insert_record('artefact_file_mime_types', $record); } } } db_commit(); //db_rollback(); } } abstract class ArtefactTypeFileBase extends ArtefactType { protected $adminfiles = 0; protected $size; // The original filename extension (when the file is first // uploaded) is saved here. This is used as a workaround for IE's // detecting filetypes by extension: when the file is downloaded, // the extension can be appended to the name if it's not there // already. protected $oldextension; public function __construct($id = 0, $data = null) { parent::__construct($id, $data); if (empty($this->id)) { $this->locked = 0; } if ($this->id && ($filedata = get_record('artefact_file_files', 'artefact', $this->id))) { foreach($filedata as $name => $value) { if (property_exists($this, $name)) { $this->{$name} = $value; } } } } public function render_self($options) { $options['id'] = $this->get('id'); $downloadpath = get_config('wwwroot') . 'artefact/file/download.php?file=' . $this->get('id'); if (isset($options['viewid'])) { $downloadpath .= '&view=' . $options['viewid']; } $filetype = get_string($this->get('oldextension'), 'artefact.file'); if (substr($filetype, 0, 2) == '[[') { $filetype = $this->get('oldextension') . ' ' . get_string('file', 'artefact.file'); } $smarty = smarty_core(); $smarty->assign('iconpath', $this->get_icon($options)); $smarty->assign('downloadpath', $downloadpath); $smarty->assign('filetype', $filetype); $smarty->assign('owner', display_name($this->get('owner'))); $smarty->assign('created', strftime(get_string('strftimedaydatetime'), $this->get('ctime'))); $smarty->assign('modified', strftime(get_string('strftimedaydatetime'), $this->get('mtime'))); $smarty->assign('size', $this->describe_size() . ' (' . $this->get('size') . ' ' . get_string('bytes', 'artefact.file') . ')'); foreach (array('title', 'description', 'artefacttype') as $field) { $smarty->assign($field, $this->get($field)); } return array('html' => $smarty->fetch('artefact:file:file_render_self.tpl'), 'javascript' => ''); } /** * This function updates or inserts the artefact. This involves putting * some data in the artefact table (handled by parent::commit()), and then * some data in the artefact_file_files table. */ public function commit() { // Just forget the whole thing when we're clean. if (empty($this->dirty)) { return; } // We need to keep track of newness before and after. $new = empty($this->id); $this->mtime = time(); // Commit to the artefact table. parent::commit(); // Reset dirtyness for the time being. $this->dirty = true; $data = (object)array( 'artefact' => $this->get('id'), 'size' => $this->get('size'), 'adminfiles' => $this->get('adminfiles'), 'oldextension' => $this->get('oldextension') ); if ($new) { insert_record('artefact_file_files', $data); } else { update_record('artefact_file_files', $data, 'artefact'); } $this->dirty = false; } public static function is_singular() { return false; } public static function get_icon($options=null) { } public static function collapse_config() { return 'file'; } public function move($newparentid) { $this->set('parent', $newparentid); $this->commit(); return true; } public function delete() { if (empty($this->id)) { return; } try { delete_records('artefact_blog_blogpost_file', 'file', $this->id); } catch ( Exception $e ) {} delete_records('artefact_file_files', 'artefact', $this->id); parent::delete(); } // Check if something exists in the db with a given title and parent, // either in adminfiles or with a specific owner. public static function file_exists($title, $owner, $folder, $adminfiles=false) { $filetypesql = "('" . join("','", PluginArtefactFile::get_artefact_types()) . "')"; return get_field_sql('SELECT a.id FROM {artefact} a LEFT OUTER JOIN {artefact_file_files} f ON f.artefact = a.id WHERE ' . ($adminfiles ? 'f.adminfiles = 1' : 'f.adminfiles <> 1 AND a.owner = ' . $owner) . ' AND a.title = ? AND a.parent ' . (empty($folder) ? ' IS NULL' : ' = ' . $folder) . ' AND a.artefacttype IN ' . $filetypesql, array($title)); } // Sort folders before files; then use nat sort order. public static function my_files_cmp($a, $b) { return strnatcasecmp((int)($a->artefacttype != 'folder') . $a->title, (int)($b->artefacttype != 'folder') . $b->title); } public static function get_my_files_data($parentfolderid, $userid, $adminfiles=false) { $foldersql = $parentfolderid ? ' = ' . $parentfolderid : ' IS NULL'; // if blogs are installed then also return the number of blog // posts each file is attached to $bloginstalled = !$adminfiles && get_field('artefact_installed', 'active', 'name', 'blog'); $filetypesql = "('" . join("','", PluginArtefactFile::get_artefact_types()) . "')"; $filedata = get_records_sql_array(' SELECT a.id, a.artefacttype, a.mtime, f.size, a.title, a.description, COUNT(c.id) AS childcount ' . ($bloginstalled ? ', COUNT (b.blogpost) AS attachcount' : '') . ' FROM {artefact} a LEFT OUTER JOIN {artefact_file_files} f ON f.artefact = a.id LEFT OUTER JOIN {artefact} c ON c.parent = a.id ' . ($bloginstalled ? ('LEFT OUTER JOIN {artefact_blog_blogpost_file} b ON b.file = a.id') : '') . ' WHERE a.parent' . $foldersql . ' AND ' . ($adminfiles ? 'f.adminfiles = 1' : ('f.adminfiles = 0 AND a.owner = ' . $userid)) . ' AND a.artefacttype IN ' . $filetypesql . ' GROUP BY 1, 2, 3, 4, 5, 6;', ''); if (!$filedata) { $filedata = array(); } else { foreach ($filedata as $item) { $item->mtime = format_date(strtotime($item->mtime), 'strfdaymonthyearshort'); $item->tags = get_column('artefact_tag', 'tag', 'artefact', $item->id); if (!is_array($item->tags)) { $item->tags = array(); } } } // Add parent folder to the list if (!empty($parentfolderid)) { $grandparentid = get_field('artefact', 'parent', 'id', $parentfolderid); $filedata[] = (object) array( 'title' => '..', 'artefacttype' => 'folder', 'description' => get_string('parentfolder', 'artefact.file'), 'isparent' => true, 'id' => (int) $grandparentid ); } usort($filedata, array("ArtefactTypeFileBase", "my_files_cmp")); return $filedata; } } class ArtefactTypeFile extends ArtefactTypeFileBase { public function __construct($id = 0, $data = null) { parent::__construct($id, $data); if (empty($this->id)) { $this->container = 0; } } public static function get_file_directory($id) { return "artefact/file/originals/" . ($id % 256); } public function get_path() { return get_config('dataroot') . self::get_file_directory($this->id) . '/' . $this->id; } public static function detect_artefact_type($file) { require_once('file.php'); if (ArtefactTypeImage::is_image_mime_type(get_mime_type(get_config('dataroot') . $file))) { return 'image'; } return 'file'; } /** * Test file type and return a new Image or File. */ public static function new_file($path, $data) { require_once('file.php'); $type = get_mime_type($path); if (ArtefactTypeImage::is_image_mime_type($type)) { list($data->width, $data->height) = getimagesize($path); return new ArtefactTypeImage(0, $data); } return new ArtefactTypeFile(0, $data); } /** * Moves a file into the myfiles area. * Takes the name of a file outside the myfiles area. * Returns a boolean indicating success or failure. */ public static function save_file($pathname, $data) { // This is only used when blog posts are saved: Files which // have been uploaded to the post are moved to a permanent // location in the files area using this function. $dataroot = get_config('dataroot'); $pathname = $dataroot . $pathname; if (!$size = filesize($pathname)) { return false; } $f = self::new_file($pathname, $data); $f->set('size', $size); $f->commit(); $id = $f->get('id'); $newdir = $dataroot . self::get_file_directory($id); check_dir_exists($newdir); $newname = $newdir . '/' . $id; if (!rename($pathname, $newname)) { $f->delete(); return false; } global $USER; $USER->quota_add($size); $USER->commit(); return $id; } /** * Processes a newly uploaded file, copies it to disk, and creates * a new artefact object. * Takes the name of a file input. * Returns false for no errors, or a string describing the error. */ public static function save_uploaded_file($inputname, $data) { require_once('uploadmanager.php'); $um = new upload_manager($inputname); if ($error = $um->preprocess_file()) { return $error; } $size = $um->file['size']; global $USER; if (!$USER->quota_allowed($size) && !$data->adminfiles) { return get_string('uploadexceedsquota'); } $f = self::new_file($um->file['tmp_name'], $data); $f->set('owner', $USER->id); $f->set('size', $size); $f->set('oldextension', $um->original_filename_extension()); $f->commit(); $id = $f->get('id'); // Save the file using its id as the filename, and use its id modulo // the number of subdirectories as the directory name. if ($error = $um->save_file(self::get_file_directory($id) , $id)) { $f->delete(); } else { $USER->quota_add($size); $USER->commit(); } return $error; } // Return the title with the original extension appended to it if // it's not already there. public function download_title() { $extn = $this->get('oldextension'); $name = $this->get('title'); if (substr($name, -1-strlen($extn)) == '.' . $extn) { return $name; } return $name . (substr($name, -1) == '.' ? '' : '.') . $extn; } public static function get_admin_files($public) { $pubfolder = ArtefactTypeFolder::admin_public_folder_id(); $artefacts = get_records_sql_assoc(' SELECT a.id, a.title, a.parent, a.artefacttype FROM {artefact} a INNER JOIN {artefact_file_files} f ON f.artefact = a.id WHERE f.adminfiles = 1', array()); $files = array(); if (!empty($artefacts)) { foreach ($artefacts as $a) { if ($a->artefacttype != 'folder') { $title = $a->title; $parent = $a->parent; while (!empty($parent)) { if ($public && $parent == $pubfolder) { $files[] = array('name' => $title, 'id' => $a->id); continue 2; } $title = $artefacts[$parent]->title . '/' . $title; $parent = $artefacts[$parent]->parent; } if (!$public) { $files[] = array('name' => $title, 'id' => $a->id); } } } } return $files; } public function delete() { if (empty($this->id)) { return; } $file = $this->get_path(); // Detach this file from any view feedback set_field('view_feedback', 'attachment', null, 'attachment', $this->id); if (is_file($file)) { $size = filesize($file); unlink($file); global $USER; // Deleting other users' files won't lower their quotas yet... if (!$this->adminfiles && $USER->id == $this->get('owner')) { $USER->quota_remove($size); $USER->commit(); } } parent::delete(); } public static function has_config() { return true; } public static function get_icon($options=null) { return theme_get_url('images/file.gif'); } public static function get_config_options() { $elements = array(); $defaultquota = get_config_plugin('artefact', 'file', 'defaultquota'); if (empty($defaultquota)) { $defaultquota = 1024 * 1024 * 10; } $elements['quotafieldset'] = array( 'type' => 'fieldset', 'legend' => get_string('defaultquota', 'artefact.file'), 'elements' => array( 'defaultquotadescription' => array( 'value' => '