. * * @package mahara * @subpackage artefact-internal * @author Catalyst IT Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL * @copyright (C) 2006-2009 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', 'profileicon', 'archive', 'video', 'audio', ); } public static function get_block_types() { return array('image'); } public static function get_plugin_name() { return 'file'; } public static function menu_items() { return array( 'content/files' => array( 'path' => 'content/files', 'url' => 'artefact/file/', 'title' => get_string('Files', 'artefact.file'), 'weight' => 30, ), 'content/profileicons' => array( 'path' => 'content/profileicons', 'url' => 'artefact/file/profileicons.php', 'title' => get_string('profileicons', 'artefact.file'), 'weight' => 20, ), ); } public static function group_tabs($groupid) { return array( 'files' => array( 'path' => 'groups/files', 'url' => 'artefact/file/groupfiles.php?group='.$groupid, 'title' => get_string('Files', 'artefact.file'), 'weight' => 70, ), ); } public static function get_event_subscriptions() { $subscriptions = array( (object)array( 'plugin' => 'file', 'event' => 'createuser', 'callfunction' => 'newuser', ), ); return $subscriptions; } public static function postinst($prevversion) { if ($prevversion == 0) { // Set default quotas to 50MB set_config_plugin('artefact', 'file', 'defaultquota', 52428800); set_config_plugin('artefact', 'file', 'defaultgroupquota', 52428800); } set_config_plugin('artefact', 'file', 'commentsallowedimage', 1); 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 jsstrings($type) { static $jsstrings = array( 'filebrowser' => array( 'mahara' => array( 'remove', ), 'artefact.file' => array( 'confirmdeletefile', 'confirmdeletefolder', 'confirmdeletefolderandcontents', 'defaultprofileicon', 'editfile', 'editfolder', 'fileappearsinviews', 'fileattachedtoportfolioitems', 'filewithnameexists', 'folderappearsinviews', 'foldercontainsprofileicons', 'foldernamerequired', 'foldernotempty', 'maxuploadsize', 'nametoolong', 'namefieldisrequired', 'upload', 'uploadingfiletofolder', 'youmustagreetothecopyrightnotice', ), ), ); return $jsstrings[$type]; } public static function jshelp($type) { static $jshelp = array( 'filebrowser' => 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(); $currentlist = get_records_assoc('artefact_file_mime_types'); $newlist = xmlize(file_get_contents(get_config('docroot') . 'artefact/file/filetypes.xml')); $filetypes = $newlist['filetypes']['#']['filetype']; $newtypes = array(); $count = array('added' => 0, 'updated' => 0, 'removed' => 0); // Step one: if a mimetype is in the new list that is not in the current // list, add it to the current list. foreach ($filetypes as $filetype) { $description = $filetype['#']['description'][0]['#']; foreach ($filetype['#']['mimetypes'][0]['#']['mimetype'] as $type) { $mimetype = $type['#']; if (!isset($currentlist[$mimetype])) { execute_sql("INSERT INTO {artefact_file_mime_types} (mimetype, description) VALUES (?,?)", array($mimetype, $description)); $count['added']++; } else if ($currentlist[$mimetype]->description != $description) { execute_sql("UPDATE {artefact_file_mime_types} SET description = ? WHERE mimetype = ?", array($description, $mimetype)); $count['updated']++; } $newtypes[$mimetype] = true; $currentlist[$mimetype] = (object) array( 'mimetype' => $mimetype, 'description' => $description, ); } } // Step two: If a mimetype is in the current list that is not in the // new list, remove it from the current list. foreach ($currentlist as $mimetype => $type) { if (!isset($newtypes[$mimetype])) { delete_records('artefact_file_mime_types', 'mimetype', $mimetype); $count['removed']++; } } db_commit(); $changes = array(); foreach (array_filter($count) as $k => $v) { $changes[] = "$v $k"; } if ($changes) { log_info('Updated filetype list: ' . join(', ', $changes) . '.'); } } public static function get_mimetypes_from_description($description=null, $getrecords=false) { static $allmimetypes = null; if (is_null($allmimetypes)) { $allmimetypes = get_records_assoc('artefact_file_mime_types'); } if (is_string($description)) { $description = array($description); } $mimetypes = array(); foreach ($allmimetypes as $r) { if (is_null($description) || in_array($r->description, $description)) { if ($getrecords) { $mimetypes[$r->mimetype] = $r; } else { $mimetypes[] = $r->mimetype; } } } return $mimetypes; } public static function can_be_disabled() { return false; } public static function get_artefact_type_content_types() { return array( 'file' => array('file'), 'image' => array('file', 'image'), 'profileicon' => array('file', 'image'), 'archive' => array('file'), 'video' => array('file'), 'audio' => array('file'), ); } public static function get_attachment_types() { return array( 'file', 'image', 'archive', 'video', 'audio' ); } public static function recalculate_quota() { $data = get_records_sql_assoc(" SELECT a.owner, SUM(f.size) AS bytes FROM {artefact} a JOIN {artefact_file_files} f ON a.id = f.artefact WHERE a.artefacttype IN (" . join(',', array_map('db_quote', PluginArtefactFile::get_artefact_types())) . ") AND a.owner IS NOT NULL GROUP BY a.owner", array() ); if ($data) { return array_map(create_function('$a', 'return $a->bytes;'), $data); } return array(); } public static function recalculate_group_quota() { $data = get_records_sql_assoc(" SELECT a.group, SUM(f.size) AS bytes FROM {artefact} a JOIN {artefact_file_files} f ON a.id = f.artefact WHERE a.artefacttype IN (" . join(',', array_map('db_quote', PluginArtefactFile::get_artefact_types())) . ") AND a.group IS NOT NULL GROUP BY a.group", array() ); if ($data) { return array_map(create_function('$a', 'return $a->bytes;'), $data); } return array(); } } abstract class ArtefactTypeFileBase extends ArtefactType { public function __construct($id = 0, $data = null) { parent::__construct($id, $data); if (empty($this->id)) { $this->allowcomments = get_config_plugin('artefact', 'file', 'commentsallowed' . $this->artefacttype); } } 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; } // 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, $institution=null, $group=null) { $filetypesql = "('" . join("','", PluginArtefactFile::get_artefact_types()) . "')"; $ownersql = artefact_owner_sql($owner, $group, $institution); return get_field_sql('SELECT a.id FROM {artefact} a LEFT OUTER JOIN {artefact_file_files} f ON f.artefact = a.id WHERE a.title = ? AND a.' . $ownersql . ' AND a.parent ' . (empty($folder) ? ' IS NULL' : ' = ' . (int)$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((-2 * isset($a->isparent) + ($a->artefacttype != 'folder')) . $a->title, (-2 * isset($b->isparent) + ($b->artefacttype != 'folder')) . $b->title); } /** * Gets a list of files in one folder * * @param integer $parentfolderid Artefact id of the folder * @param integer $userid Id of the owner, if the owner is a user * @param integer $group Id of the owner, if the owner is a group * @param string $institution Id of the owner, if the owner is a institution * @param array $filters Filters to apply to the results. An array with keys 'artefacttype', 'filetype', * where array values are arrays of artefacttype or mimetype strings. * @return array A list of artefacts */ public static function get_my_files_data($parentfolderid, $userid, $group=null, $institution=null, $filters=null) { global $USER; $select = ' SELECT a.id, a.artefacttype, a.mtime, f.size, a.title, a.description, a.locked, a.allowcomments, u.profileicon AS defaultprofileicon, COUNT(DISTINCT c.id) AS childcount, COUNT (DISTINCT aa.artefact) AS attachcount, COUNT(DISTINCT va.view) AS viewcount, COUNT(DISTINCT api.id) AS profileiconcount'; $from = ' 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 LEFT OUTER JOIN {artefact} api ON api.parent = a.id AND api.artefacttype = \'profileicon\' LEFT OUTER JOIN {view_artefact} va ON va.artefact = a.id LEFT OUTER JOIN {artefact_attachment} aa ON aa.attachment = a.id LEFT OUTER JOIN {usr} u ON a.id = u.profileicon AND a.owner = u.id'; if (!empty($filters['artefacttype'])) { $artefacttypes = $filters['artefacttype']; $artefacttypes[] = 'folder'; } else { $artefacttypes = PluginArtefactFile::get_artefact_types(); } $where = " WHERE a.artefacttype IN (" . join(',', array_map('db_quote', $artefacttypes)) . ")"; if (!empty($filters['filetype']) && is_array($filters['filetype'])) { $where .= " AND (a.artefacttype = 'folder' OR f.filetype IN (" . join(',', array_map('db_quote', $filters['filetype'])) . '))'; } $groupby = ' GROUP BY a.id, a.artefacttype, a.mtime, f.size, a.title, a.description, a.locked, a.allowcomments, u.profileicon'; $phvals = array(); if ($institution) { if ($institution == 'mahara' && !$USER->get('admin')) { // If non-admins are browsing site files, only let them see the public folder & its contents $publicfolder = ArtefactTypeFolder::admin_public_folder_id(); $from .= ' LEFT OUTER JOIN {artefact_parent_cache} pub ON (a.id = pub.artefact AND pub.parent = ?)'; $where .= ' AND (pub.parent = ? OR a.id = ?)'; $phvals = array($publicfolder, $publicfolder, $publicfolder); } $where .= ' AND a.institution = ? AND a.owner IS NULL'; $phvals[] = $institution; } else if ($group) { $select .= ', r.can_edit, r.can_view, r.can_republish, a.author'; $from .= ' LEFT OUTER JOIN ( SELECT ar.artefact, ar.can_edit, ar.can_view, ar.can_republish FROM {artefact_access_role} ar INNER JOIN {group_member} gm ON ar.role = gm.role WHERE gm.group = ? AND gm.member = ? ) r ON r.artefact = a.id'; $phvals[] = $group; $phvals[] = $USER->get('id'); $where .= ' AND a.group = ? AND a.owner IS NULL AND (r.can_view = 1 OR a.author = ?)'; $phvals[] = $group; $phvals[] = $USER->get('id'); $groupby .= ', r.can_edit, r.can_view, r.can_republish, a.author'; } else { $where .= ' AND a.institution IS NULL AND a.owner = ?'; $phvals[] = $userid; } if ($parentfolderid) { $where .= ' AND a.parent = ? '; $phvals[] = $parentfolderid; } else { $where .= ' AND a.parent IS NULL'; } $filedata = get_records_sql_assoc($select . $from . $where . $groupby, $phvals); if (!$filedata) { $filedata = array(); } else { foreach ($filedata as $item) { $item->mtime = format_date(strtotime($item->mtime), 'strfdaymonthyearshort'); $item->tags = array(); $item->allowcomments = (bool) $item->allowcomments; $item->icon = call_static_method(generate_artefact_class_name($item->artefacttype), 'get_icon', array('id' => $item->id)); if ($item->size) { // Doing this here now for non-js users $item->size = ArtefactTypeFile::short_size($item->size, true); } if ($group && $item->author == $USER->get('id')) { $item->can_edit = 1; // This will show the delete, edit buttons in filelist, but doesn't change the actual permissions in the checkbox } } $where = 'artefact IN (' . join(',', array_keys($filedata)) . ')'; $tags = get_records_select_array('artefact_tag', $where); if ($tags) { foreach ($tags as $t) { $filedata[$t->artefact]->tags[] = $t->tag; } } if ($group) { // Fetch permissions for each artefact $perms = get_records_select_array('artefact_access_role', $where); if ($perms) { foreach ($perms as $perm) { $filedata[$perm->artefact]->permissions[$perm->role] = array( 'view' => $perm->can_view, 'edit' => $perm->can_edit, 'republish' => $perm->can_republish ); } } } } // Add parent folder to the list if (!empty($parentfolderid)) { $grandparentid = (int) get_field('artefact', 'parent', 'id', $parentfolderid); $filedata[$grandparentid] = (object) array( 'title' => get_string('parentfolder', 'artefact.file'), 'artefacttype' => 'folder', 'description' => get_string('parentfolder', 'artefact.file'), 'isparent' => true, 'id' => $grandparentid, 'icon' => ArtefactTypeFolder::get_icon(), ); } uasort($filedata, array("ArtefactTypeFileBase", "my_files_cmp")); return $filedata; } /** * Creates pieforms definition for forms on the my files, group files, etc. pages. */ public static function files_form($page='', $group=null, $institution=null, $folder=null, $highlight=null, $edit=null) { $folder = param_integer('folder', 0); $edit = param_variable('edit', 0); if (is_array($edit)) { $edit = array_keys($edit); $edit = $edit[0]; } $edit = (int) $edit; $highlight = null; if ($file = param_integer('file', 0)) { $highlight = array($file); // todo convert to file1=1&file2=2 etc } $form = array( 'name' => 'files', 'jsform' => true, 'newiframeonsubmit' => true, 'jssuccesscallback' => 'files_callback', 'jserrorcallback' => 'files_callback', 'renderer' => 'oneline', 'plugintype' => 'artefact', 'pluginname' => 'file', 'configdirs' => array(get_config('libroot') . 'form/', get_config('docroot') . 'artefact/file/form/'), 'group' => $group, 'institution' => $institution, 'elements' => array( 'filebrowser' => array( 'type' => 'filebrowser', 'folder' => $folder, 'highlight' => $highlight, 'edit' => $edit, 'page' => $page, 'config' => array( 'upload' => true, 'uploadagreement' => get_config_plugin('artefact', 'file', 'uploadagreement'), 'createfolder' => true, 'edit' => true, 'select' => false, ), ), ), ); return $form; } public static function files_js() { return "function files_callback(form, data) { files_filebrowser.callback(form, data); }"; } public static function count_user_files($owner=null, $group=null, $institution=null) { $filetypes = PluginArtefactFile::get_artefact_types(); foreach ($filetypes as $k => $v) { if ($v == 'folder') { unset($filetypes[$k]); } } $filetypesql = "('" . join("','", $filetypes) . "')"; $ownersql = artefact_owner_sql($owner, $group, $institution); return (object) array( 'files' => count_records_select('artefact', "artefacttype IN $filetypesql AND $ownersql", array()), 'folders' => count_records_select('artefact', "artefacttype = 'folder' AND $ownersql", array()) ); } public static function artefactchooser_get_file_data($artefact) { $artefact->icon = call_static_method(generate_artefact_class_name($artefact->artefacttype), 'get_icon', array('id' => $artefact->id)); if ($artefact->artefacttype == 'profileicon') { $artefact->hovertitle = $artefact->note; if ($artefact->title) { $artefact->hovertitle .= ': ' . $artefact->title; } } else { $artefact->hovertitle = $artefact->title; if ($artefact->description) { $artefact->hovertitle .= ': ' . $artefact->description; } } $folderdata = self::artefactchooser_folder_data($artefact); if ($artefact->artefacttype == 'profileicon') { $artefact->description = str_shorten_text($artefact->title, 30); } else { $path = $artefact->parent ? self::get_full_path($artefact->parent, $folderdata->data) : ''; $artefact->description = str_shorten_text($folderdata->ownername . $path . $artefact->title, 30); } return $artefact; } public static function artefactchooser_folder_data(&$artefact) { // Grab data about all folders the artefact owner has, so we // can make full paths to them, and show the artefact owner if // it's a group or institution. static $folderdata = array(); $ownerkey = $artefact->owner . '::' . $artefact->group . '::' . $artefact->institution; if (!isset($folderdata[$ownerkey])) { $ownersql = artefact_owner_sql($artefact->owner, $artefact->group, $artefact->institution); $folderdata[$ownerkey] = new stdClass(); $folderdata[$ownerkey]->data = get_records_select_assoc('artefact', "artefacttype='folder' AND $ownersql", array(), '', 'id, title, parent'); if ($artefact->group) { $folderdata[$ownerkey]->ownername = get_field('group', 'name', 'id', $artefact->group) . ':'; } else if ($artefact->institution) { if ($artefact->institution == 'mahara') { $folderdata[$ownerkey]->ownername = get_config('sitename') . ':'; } else { $folderdata[$ownerkey]->ownername = get_field('institution', 'displayname', 'name', $artefact->institution) . ':'; } } else { $folderdata[$ownerkey]->ownername = ''; } } return $folderdata[$ownerkey]; } /** * Works out a full path to a folder, given an ID. Implemented this way so * only one query is made. */ public static function get_full_path($id, &$folderdata) { $path = ''; while (!empty($id)) { $path = $folderdata[$id]->title . '/' . $path; $id = $folderdata[$id]->parent; } return $path; } public function default_parent_for_copy(&$view, &$template, $artefactstoignore) { static $folderids; $viewid = $view->get('id'); if (isset($folderids[$viewid])) { return $folderids[$viewid]; } $viewfilesfolder = ArtefactTypeFolder::get_folder_id(get_string('viewfilesdirname', 'view'), get_string('viewfilesdirdesc', 'view'), null, true, $view->get('owner'), $view->get('group'), $view->get('institution'), $artefactstoignore); $foldername = $viewid; $existing = get_column_sql(" SELECT title FROM {artefact} WHERE parent = ? AND title LIKE ? || '%'", array($viewfilesfolder, $foldername)); $sep = ''; $ext = ''; if ($existing) { while (in_array($foldername . $sep . $ext, $existing)) { $sep = '-'; $ext++; } } $data = (object) array( 'title' => $foldername . $sep . $ext, 'description' => get_string('filescopiedfromviewtemplate', 'view', $template->get('title')), 'owner' => $view->get('owner'), 'group' => $view->get('group'), 'institution' => $view->get('institution'), 'parent' => $viewfilesfolder, ); $folder = new ArtefactTypeFolder(0, $data); $folder->commit(); $folderids[$viewid] = $folder->get('id'); return $folderids[$viewid]; } /** * Return a unique artefact title for a given owner & parent. * * Try to add digits before the filename extension: If the desired * title contains a ".", add "." plus digits before the final ".", * otherwise append "." and digits. * * @param string $desired * @param integer $parent * @param integer $owner * @param integer $group * @param string $institution */ public static function get_new_file_title($desired, $parent, $owner=null, $group=null, $institution=null) { $bits = split('\.', $desired); if (count($bits) > 1 && preg_match('/[^0-9]/', end($bits))) { $start = join('.', array_slice($bits, 0, count($bits)-1)); $end = '.' . end($bits); } else { $start = $desired; $end = ''; } $where = ($parent && is_int($parent)) ? "parent = $parent" : 'parent IS NULL'; $where .= ' AND ' . artefact_owner_sql($owner, $group, $institution); $taken = get_column_sql(" SELECT title FROM {artefact} WHERE artefacttype IN ('" . join("','", PluginArtefactFile::get_artefact_types()) . "') AND title LIKE ? || '%' || ? AND " . $where, array($start, $end)); $taken = array_flip($taken); $i = 0; $newname = $start . $end; while (isset($taken[$newname])) { $i++; $newname = $start . '.' . $i . $end; } return $newname; } public static function blockconfig_filebrowser_element(&$instance, $default=array()) { return array( 'name' => 'filebrowser', 'type' => 'filebrowser', 'title' => get_string('file', 'artefact.file'), 'folder' => (int) param_variable('folder', 0), 'highlight' => null, 'browse' => true, 'page' => get_config('wwwroot') . 'view/blocks.php' . View::make_base_url(), 'config' => array( 'upload' => true, 'uploadagreement' => get_config_plugin('artefact', 'file', 'uploadagreement'), 'createfolder' => false, 'edit' => false, 'tag' => true, 'select' => true, 'alwaysopen' => true, 'publishing' => true, ), 'tabs' => $instance->get_view()->ownership(), 'defaultvalue' => $default, 'selectlistcallback' => 'artefact_get_records_by_id', ); } } class ArtefactTypeFile extends ArtefactTypeFileBase { 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; // The id used for the filename on the filesystem. Usually this // is the same as the artefact id, but it can be different if the // file is a copy of another file artefact. protected $fileid; protected $filetype; // Mime type public function __construct($id = 0, $data = null) { parent::__construct($id, $data); 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; } } } } /** * 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); // 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'), 'oldextension' => $this->get('oldextension'), 'fileid' => $this->get('fileid'), 'filetype' => $this->get('filetype'), ); if ($new) { if (empty($data->fileid)) { $data->fileid = $data->artefact; } insert_record('artefact_file_files', $data); } else { update_record('artefact_file_files', $data, 'artefact'); } $this->dirty = false; } 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->fileid) . '/' . $this->fileid; } /** * Test file type and return a new Image or File. */ public static function new_file($path, $data) { require_once('file.php'); if (is_image_file($path)) { // If it's detected as an image, overwrite the browser mime type $imageinfo = getimagesize($path); $data->filetype = $imageinfo['mime']; $data->width = $imageinfo[0]; $data->height = $imageinfo[1]; return new ArtefactTypeImage(0, $data); } // If $data->filetype is set here, it's probably the claim made by the // browser during upload or by a Leap2a file. Generally we believe this // claim, because it gives better results when serving the file on // download. If there's no claimed mimetype, use file_mime_type to make // a guess, and give each file artefact type access to both the claimed // and guessed mimetypes. $data->guess = file_mime_type($path); if (empty($data->filetype) || $data->filetype == 'application/octet-stream') { $data->filetype = $data->guess; } foreach (array('video', 'audio', 'archive') as $artefacttype) { $classname = 'ArtefactType' . ucfirst($artefacttype); if (call_static_method($classname, 'is_valid_file', $path, &$data)) { return new $classname(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. * * Note: this method is crappy because it returns false instead of throwing * exceptions. It's not used in many places, and should probably die in a * future version. So think twice before using it :) */ public static function save_file($pathname, $data, User &$user=null, $outsidedataroot=false) { global $USER; $dataroot = get_config('dataroot'); if (!$outsidedataroot) { $pathname = $dataroot . $pathname; } if (!file_exists($pathname) || !is_readable($pathname)) { return false; } $size = filesize($pathname); $f = self::new_file($pathname, $data); $f->set('size', $size); // if an extension has been provided (only from self::extract() at this stage), use it if (!empty($data->extension)) { $f->set('oldextension', $data->extension); } $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; } $owner = null; if ($user) { $owner = $user; } else if ($data->owner == $USER->get('id')) { $owner = $USER; $owner->quota_refresh(); } else if (!empty($data->owner)) { $owner = new User; $owner->find_by_id($data->owner); } try { if ($owner) { $owner->quota_add($size); $owner->commit(); } else if (!empty($data->group)) { require_once('group.php'); group_quota_add($data->group, $size); } return $id; } catch (QuotaExceededException $e) { $f->delete(); return false; } } /** * 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, $inputindex=null) { require_once('uploadmanager.php'); $um = new upload_manager($inputname, false, $inputindex); if ($error = $um->preprocess_file()) { throw new UploadException($error); } if (isset($inputindex)) { $size = $um->file['size'][$inputindex]; $tmpname = $um->file['tmp_name'][$inputindex]; $filetype = $um->file['type'][$inputindex]; } else { $size = $um->file['size']; $tmpname = $um->file['tmp_name']; $filetype = $um->file['type']; } if (!empty($data->owner)) { global $USER; if ($data->owner == $USER->get('id')) { $owner = $USER; $owner->quota_refresh(); } else { $owner = new User; $owner->find_by_id($data->owner); } if (!$owner->quota_allowed($size)) { throw new QuotaExceededException(get_string('uploadexceedsquota', 'artefact.file')); } } if (!empty($data->group)) { require_once('group.php'); if (!group_quota_allowed($data->group, $size)) { throw new QuotaExceededException(get_string('uploadexceedsquota', 'artefact.file')); } } $data->size = $size; $data->filetype = $filetype; $data->oldextension = $um->original_filename_extension(); $f = self::new_file($tmpname, $data); $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(); throw new UploadException($error); } else if (isset($owner)) { $owner->quota_add($size); $owner->commit(); } else if (!empty($data->group)) { group_quota_add($data->group, $size); } return $id; } // 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 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('ownername', $this->display_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', 'owner', 'tags') as $field) { $smarty->assign($field, $this->get($field)); } return array('html' => $smarty->fetch('artefact:file:file_render_self.tpl'), 'javascript' => ''); } 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 LEFT OUTER JOIN {artefact_file_files} f ON f.artefact = a.id WHERE a.institution = 'mahara'", 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(); if (is_file($file)) { $size = filesize($file); // Only delete the file on disk if no other artefacts point to it if (count_records('artefact_file_files', 'fileid', $this->get('id')) == 1) { unlink($file); } global $USER; // Deleting other users' files won't lower their quotas yet... if (!$this->institution && $USER->id == $this->get('owner')) { $USER->quota_remove($size); $USER->commit(); } if (!empty($this->group)) { require_once('group.php'); group_quota_remove($this->group, $size); } } delete_records('artefact_attachment', 'attachment', $this->id); delete_records('artefact_file_files', 'artefact', $this->id); delete_records('site_menu', 'file', $this->id); parent::delete(); } public static function bulk_delete($artefactids) { global $USER; require_once('group.php'); if (empty($artefactids)) { return; } $idstr = join(',', array_map('intval', $artefactids)); db_begin(); // Get the size of all the files we're about to delete that belong to // the user. if ($group = group_current_group()) { $totalsize = get_field_sql(' SELECT SUM(size) FROM {artefact_file_files} f JOIN {artefact} a ON f.artefact = a.id WHERE a.group = ? AND f.artefact IN (' . $idstr . ')', array($group->id) ); } else { $totalsize = get_field_sql(' SELECT SUM(size) FROM {artefact_file_files} f JOIN {artefact} a ON f.artefact = a.id WHERE a.owner = ? AND f.artefact IN (' . $idstr . ')', array($USER->get('id')) ); } // Get all fileids so that we can delete the files on disk $fileids = get_records_select_assoc('artefact_file_files', 'artefact IN (' . $idstr . ')', array()); $fileidcounts = get_records_sql_assoc(' SELECT fileid, COUNT(fileid) AS fileidcount FROM {artefact_file_files} WHERE artefact IN (' . $idstr . ') GROUP BY fileid', null ); // The current rule is that file deletion should be logged in the artefact_log table // only for group-owned files. To save time we will be slightly naughty here and // log deletion for all these files if at least one is group-owned. $log = (bool) count_records_select('artefact', 'id IN (' . $idstr . ') AND "group" IS NOT NULL'); delete_records_select('artefact_attachment', 'attachment IN (' . $idstr . ')'); delete_records_select('artefact_file_files', 'artefact IN (' . $idstr . ')'); parent::bulk_delete($artefactids, $log); foreach ($fileids as $r) { // Delete the file on disk if there's only one artefact left pointing to it if ($fileidcounts[$r->fileid]->fileidcount == 1) { $file = get_config('dataroot') . self::get_file_directory($r->fileid) . '/' . $r->fileid; if (is_file($file)) { unlink($file); } } $fileidcounts[$r->fileid]->fileidcount--; } if ($totalsize) { if ($group) { group_quota_remove($group->id, $totalsize); } else { $USER->quota_remove($totalsize); $USER->commit(); } } db_commit(); } public static function has_config() { return true; } public static function get_icon($options=null) { global $THEME; 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 * 50; } $elements['quotafieldset'] = array( 'type' => 'fieldset', 'legend' => get_string('defaultquota', 'artefact.file'), 'elements' => array( 'defaultquotadescription' => array( 'value' => '