diff --git a/admin/settings/users.php b/admin/settings/users.php
index 8ea2265..52324ff 100644
--- a/admin/settings/users.php
+++ b/admin/settings/users.php
@@ -16,6 +16,7 @@ $ADMIN->add('users', new admin_category('accounts', get_string('accounts', 'admi
 $ADMIN->add('accounts', new admin_externalpage('editusers', get_string('userlist','admin'), "$CFG->wwwroot/$CFG->admin/user.php", array('moodle/user:update', 'moodle/user:delete')));
 $ADMIN->add('accounts', new admin_externalpage('addnewuser', get_string('addnewuser'), "$securewwwroot/user/editadvanced.php?id=-1", 'moodle/user:create'));
 $ADMIN->add('accounts', new admin_externalpage('uploadusers', get_string('uploadusers'), "$CFG->wwwroot/$CFG->admin/uploaduser.php", 'moodle/site:uploadusers'));
+$ADMIN->add('accounts', new admin_externalpage('uploadpictures', get_string('uploadpictures','admin'), "$CFG->wwwroot/$CFG->admin/uploadpicture.php", 'moodle/site:uploadusers'));
 $ADMIN->add('accounts', new admin_externalpage('profilefields', get_string('profilefields','admin'), "$CFG->wwwroot/user/profile/index.php", 'moodle/site:config'));
 
 
diff --git a/admin/uploadpicture.php b/admin/uploadpicture.php
new file mode 100644
index 0000000..a7deecb
--- /dev/null
+++ b/admin/uploadpicture.php
@@ -0,0 +1,287 @@
+<?php // $Id$
+
+///////////////////////////////////////////////////////////////////////////
+//                                                                       //
+// Copyright (C) 2007 Iņaki Arenaza                                      //
+//                                                                       //
+// Based on .../admin/uploaduser.php and .../lib/gdlib.php               //
+//                                                                       //
+// This program is free software; you can redistribute it and/or modify  //
+// it under the terms of the GNU General Public License as published by  //
+// the Free Software Foundation; either version 2 of the License, or     //
+// (at your option) any later version.                                   //
+//                                                                       //
+// This program is distributed in the hope that it will be useful,       //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of        //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         //
+// GNU General Public License for more details:                          //
+//                                                                       //
+//          http://www.gnu.org/copyleft/gpl.html                         //
+//                                                                       //
+///////////////////////////////////////////////////////////////////////////
+
+require_once('../config.php');
+require_once($CFG->libdir.'/uploadlib.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/gdlib.php');
+
+define ('PIX_FILE_UPDATED', 0);
+define ('PIX_FILE_ERROR',   1);
+define ('PIX_FILE_SKIPPED', 2);
+
+$adminroot = admin_get_root();
+admin_externalpage_setup('uploadpictures', $adminroot);
+
+require_login();
+
+require_capability('moodle/site:uploadusers', get_context_instance(CONTEXT_SYSTEM, SITEID));
+
+if (! $site = get_site()) {
+    error("Could not find site-level course");
+}
+
+if (!$adminuser = get_admin()) {
+    error("Could not find site admin");
+}
+
+$strfile = get_string('file');
+$struser = get_string('user');
+$strusersupdated = get_string('usersupdated');
+$struploadpictures = get_string('uploadpictures','admin');
+$usersupdated = 0;
+$userserrors = 0;
+
+$userfields = array (
+    0 => 'username',
+    1 => 'idnumber',
+    2 => 'id' );
+
+$userfield = optional_param('userfield', 0, PARAM_INT);
+$overwritepicture = optional_param('overwritepicture', 0, PARAM_BOOL);
+
+/// Print the header
+admin_externalpage_print_header($adminroot);
+
+/// If a file has been uploaded, then process it
+$um = new upload_manager('userpicturesfile',false,false,null,false,0);
+
+if (!array_key_exists($userfield, $userfields)) {
+    notify(get_string('uploadpicture_baduserfield','admin'));
+} else {
+    if ($um->preprocess_files() && confirm_sesskey()) {
+        $filename = $um->files['userpicturesfile']['tmp_name'];
+
+        // Large files are likely to take their time and memory. Let PHP know
+        // that we'll take longer, and that the process should be recycled soon
+        // to free up memory.
+        @set_time_limit(0);
+        @raise_memory_limit("192M");
+        if (function_exists('apache_child_terminate')) {
+            @apache_child_terminate();
+        }
+
+        // Create a unique temporary directory, to process the zip file
+        // contents.
+        $zipdir = my_mktempdir($CFG->dataroot.'/temp/', 'usrpic');
+
+        // We are sure the destination file name is unique (even if we
+        // always name it userpics.zip) because we are using a unique
+        // temporary directory to place it.
+        $dstfile = $zipdir.'/userpics.zip';
+        if (!move_uploaded_file($filename, $dstfile)) {
+            notify(get_string('uploadpicture_cannotmovezip','admin'));
+            @remove_dir($zipdir);
+        } else {
+            if(!unzip_file($dstfile, $zipdir, false)) {
+                notify(get_string('uploadpicture_cannotunzip','admin'));
+                @remove_dir($zipdir);
+            } else {
+                // We don't need the zip file any longer, so delete it to make
+                // it easier to process the rest of the files inside the directory.
+                @unlink($dstfile);
+
+                $results = array ('errors' => 0,'updated' => 0);
+
+                process_directory($zipdir, $userfields[$userfield], $overwritepicture, $results);
+
+                // Finally remove the temporary directory with all the user images and print some stats.
+                @remove_dir($zipdir);
+                notify(get_string('usersupdated', 'admin') . ": " . $results['updated']);
+                notify(get_string('errors', 'admin') . ": " . $results['errors']);
+                echo '<hr />';
+            }
+        }
+    }
+}
+
+/// Print the form
+print_heading_with_help($struploadpictures, 'uploadpictures');
+
+$noyesoptions = array( get_string('no'), get_string('yes') );
+
+$maxuploadsize = get_max_upload_file_size();
+echo '<div style="text-align:center">';
+echo '<form method="post" enctype="multipart/form-data" action="uploadpicture.php"><div>'.
+$strfile.'&nbsp;<input type="hidden" name="MAX_FILE_SIZE" value="'.$maxuploadsize.'" />'.
+'<input type="hidden" name="sesskey" value="'.$USER->sesskey.'" />'.
+'<input type="file" name="userpicturesfile" size="30" /><br />';
+print_heading(get_string('settings'));
+echo '<table style="margin-left:auto;margin-right:auto">';
+echo '<tr><td>' . get_string('uploadpicture_userfield', 'admin') . '</td><td>';
+choose_from_menu ($userfields, 'userfiled', $userfield);
+echo '</td></tr>';
+echo '<tr><td>' . get_string ('uploadpicture_overwrite', 'admin') . '</td><td>';
+choose_from_menu($noyesoptions, 'overwritepicture', $overwritepicture);
+echo '</td></tr>';
+echo '</table><br />';
+echo '<input type="submit" value="'.$struploadpictures.'" />';
+echo '</div></form><br />';
+echo '</div>';
+
+admin_externalpage_print_footer($adminroot);
+
+
+// ----------- Internal functions ----------------
+
+/**
+ * Create a unique temporary directory with a given prefix name,
+ * inside a given directory, with given permissions. Return the
+ * full path to the newly created temp directory.
+ *
+ * @param string $dir where to create the temp directory.
+ * @param string $prefix prefix for the temp directory name (default '')
+ * @param string $mode permissions for the temp directory (default 700)
+ *
+ * @return string The full path to the temp directory.
+ */
+function my_mktempdir($dir, $prefix='', $mode=0700) {
+    if (substr($dir, -1) != '/') {
+        $dir .= '/';
+    }
+
+    do {
+        $path = $dir.$prefix.mt_rand(0, 9999999);
+    } while (!mkdir($path, $mode));
+
+    return $path;
+}
+
+/**
+ * Recursively process a directory, picking regular files and feeding
+ * them to process_file().
+ *
+ * @param string $dir the full path of the directory to process
+ * @param string $userfield the prefix_user table field to use to
+ *               match picture files to users.
+ * @param bool $overwrite overwrite existing picture or not.
+ * @param array $results (by reference) accumulated statistics of
+ *              users updated and errors.
+ *
+ * @return nothing
+ */
+function process_directory ($dir, $userfield, $overwrite, &$results) {
+    if(!($handle = opendir($dir))) {
+        notify(get_string('uploadpicture_cannotprocessdir','admin'));
+        return;
+    }
+
+    while (false !== ($item = readdir($handle))) {
+        if ($item != '.' && $item != '..') {
+            if (is_dir($dir.'/'.$item)) {
+                process_directory($dir.'/'.$item, $userfield, $overwrite, $results);
+            } else if (is_file($dir.'/'.$item))  {
+                $result = process_file($dir.'/'.$item, $userfield, $overwrite);
+                switch ($result) {
+                    case PIX_FILE_ERROR:
+                        $results['errors']++;
+                        break;
+                    case PIX_FILE_UPDATED:
+                        $results['updated']++;
+                        break;
+                }
+            }
+            // Ignore anything else that is not a directory or a file (e.g.,
+            // symbolic links, sockets, pipes, etc.)
+        }
+    }
+    closedir($handle);
+}
+
+/**
+ * Given the full path of a file, try to find the user the file
+ * corresponds to and assign him/her this file as his/her picture.
+ * Make extensive checks to make sure we don't open any security holes
+ * and report back any success/error.
+ *
+ * @param string $file the full path of the file to process
+ * @param string $userfield the prefix_user table field to use to
+ *               match picture files to users.
+ * @param bool $overwrite overwrite existing picture or not.
+ *
+ * @return integer either PIX_FILE_UPDATED, PIX_FILE_ERROR or
+ *                  PIX_FILE_SKIPPED
+ */
+function process_file ($file, $userfield, $overwrite) {
+    // Add additional checks on the filenames, as they are user
+    // controlled and we don't want to open any security holes.
+    $path_parts = pathinfo(cleardoubleslashes($file));
+    $basename  = $path_parts['basename'];
+    $extension = $path_parts['extension'];
+    if ($basename != clean_param($basename, PARAM_CLEANFILE)) {
+        // The original picture file name has invalid characters
+        notify(get_string('uploadpicture_invalidfilename', 'admin',
+                          clean_param($basename, PARAM_CLEANHTML)));
+        return PIX_FILE_ERROR;
+    }
+
+    // The picture file name (without extension) must match the
+    // userfield attribute.
+    $uservalue = substr($basename, 0,
+                        strlen($basename) -
+                        strlen($extension) - 1);
+
+    // userfield names are safe, so don't quote them.
+    if (!($user = get_record('user', $userfield, addslashes($uservalue)))) {
+        $a = new Object();
+        $a->userfield = clean_param($userfield, PARAM_CLEANHTML);
+        $a->uservalue = clean_param($uservalue, PARAM_CLEANHTML);
+        notify(get_string('uploadpicture_usernotfound', 'admin', $a));
+        return PIX_FILE_ERROR;
+    }
+
+    $haspicture = get_field('user', 'picture', 'id', $user->id);
+    if ($haspicture && !$overwrite) {
+        notify(get_string('uploadpicture_userskipped', 'admin', $user->username));
+        return PIX_FILE_SKIPPED;
+    }
+
+    if (my_save_profile_image($user->id, $file)) {
+        set_field('user', 'picture', 1, 'id', $user->id);
+        notify(get_string('uploadpicture_userupdated', 'admin', $user->username));
+        return PIX_FILE_UPDATED;
+    } else {
+        notify(get_string('uploadpicture_cannotsave', 'admin', $user->username));
+        return PIX_FILE_ERROR;
+    }
+}
+
+/**
+ * Try to save the given file (specified by its full path) as the
+ * picture for the user with the given id.
+ *
+ * @param integer $id the internal id of the user to assign the
+ *                picture file to.
+ * @param string $originalfile the full path of the picture file.
+ *
+ * @return bool 
+ */
+function my_save_profile_image($id, $originalfile) {
+    $destination = create_profile_image_destination($id, 'user');
+    if ($destination === false) {
+        return false;
+    }
+
+    return process_profile_image($originalfile, $destination);
+}
+
+?>
diff --git a/lang/en_utf8/admin.php b/lang/en_utf8/admin.php
index 390bc91..af56922 100644
--- a/lang/en_utf8/admin.php
+++ b/lang/en_utf8/admin.php
@@ -573,6 +573,18 @@ $string['unsupported'] = 'Unsupported';
 $string['updateaccounts'] = 'Update existing accounts';
 $string['updatecomponent'] = 'Update Component';
 $string['updatelangs'] = 'Update all local language packs';
+$string['uploadpictures'] = 'Upload user pictures';
+$string['uploadpicture_baduserfield'] = 'The user attribute specified is not valid. Please, try again.';
+$string['uploadpicture_cannotmovezip'] = 'Cannot move zip file to temporary directory.';
+$string['uploadpicture_cannotprocessdir'] = 'Cannot process unzipped files.';
+$string['uploadpicture_cannotunzip'] = 'Cannot unzip pictures file.';
+$string['uploadpicture_invalidfilename'] = 'Picture file $a has invalid characters in its name. Skipping.';
+$string['uploadpicture_overwrite'] = 'Overwrite existing user pictures?';
+$string['uploadpicture_userfield'] = 'User attribute to use to match pictures:';
+$string['uploadpicture_usernotfound'] = 'User with a \'$a->userfield\' value of \'$a->uservalue\' does not exist. Skipping.';
+$string['uploadpicture_userskipped'] = 'Skipping user $a (already has a picture).';
+$string['uploadpicture_userupdated'] = 'Picture updated for user $a.';
+$string['uploadpicture_cannotsave'] = 'Cannot save picture for user $a. Check original picture file.';
 $string['updatetimezones'] = 'Update timezones';
 $string['upgradeforumread'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.<br />To use this functionality you need to <a href=\"$a\">update your tables</a>.';
 $string['upgradeforumreadinfo'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.  To use this functionality you need to update your tables with all the tracking information for existing posts.  Depending on the size of your site this can take a long time (hours) and can be quite taxing on the database, so it\'s best to do it during a quiet period.  However, your site will continue functioning during this upgrade and users won\'t be affected.  Once you start this process you should let it finish (keep your browser window open).  However, if you stop the process by closing the window: don\'t worry, you can start over.<br /><br />Do you want to start the upgrading process now?';
diff --git a/lib/gdlib.php b/lib/gdlib.php
index 29f8628..e1f228b 100644
--- a/lib/gdlib.php
+++ b/lib/gdlib.php
@@ -72,27 +72,18 @@ function ImageCopyBicubic ($dst_img, $src_img, $dst_x, $dst_y, $src_x, $src_y, $
 }
 
 /** 
- * Given an upload manager with the right settings, this function performs a virus scan, and then scales and crops
- * it and saves it in the right place to be a "user" or "group" image.
+ * Given a user or group id and a destination directory, it creates the user 
+ * or group directory needed to store its associated profile image, and returns
+ * the full path to that directory.
  *
  * @uses $CFG
- * @param int $id description?
- * @param object $uploadmanager description?
- * @param string $dir description?
- * @return boolean
- * @todo Finish documenting this function
+ * @param int $id user or group id
+ * @param string $dir 'user' or group directory name
+ * @return string $destination (profile image destination directory path) or false on error
  */
-function save_profile_image($id, $uploadmanager, $dir='user') {
+function create_profile_image_destination($id, $dir='user') {
     global $CFG;
 
-    if (empty($CFG->gdversion)) {
-        return false;
-    }
-
-    if (!$uploadmanager) {
-        return false;
-    }
-
     umask(0000);
 
     if (!file_exists($CFG->dataroot .'/'. $dir)) {
@@ -112,12 +103,56 @@ function save_profile_image($id, $uploadmanager, $dir='user') {
             return false;
         }
     }
+    return $destination;
+}
+
+/**
+ * Given an upload manager with the right settings, this function performs a virus scan, and then scales and crops
+ * it and saves it in the right place to be a "user" or "group" image.
+ *
+ * @param int $id user or group id
+ * @param object $uploadmanager object referencing the image
+ * @param string $dir type of entity - groups, user, ...
+ * @return boolean success
+ * @todo Finish documenting this function
+ */
+function save_profile_image($id, $uploadmanager, $dir='user') {
+
+    if (!$uploadmanager) {
+        return false;
+    }
+
+    $destination = create_profile_image_destination($id, $dir);
+    if ($destination === false) {
+        return false;
+    }
 
     if (!$uploadmanager->save_files($destination)) {
         return false;
     }
 
-    $originalfile = $uploadmanager->get_new_filepath();
+    return process_profile_image($uploadmanager->get_new_filepath(), $destination);
+}
+
+/**
+ * Given a path to an image file this function scales and crops it and saves it in
+ * the right place to be a "user" or "group" image.
+ *
+ * @uses $CFG
+ * @param string $originalfile the path of the original image file
+ * @param string $destination the final destination directory of the profile image
+ * @return boolean
+ */
+function process_profile_image($originalfile, $destination) {
+    global $CFG;
+
+    if(!(is_file($originalfile) && is_dir($destination))) {
+        return false;
+    }
+
+    if (empty($CFG->gdversion)) {
+        return false;
+    }
 
     $imageinfo = GetImageSize($originalfile);
     
@@ -133,7 +168,7 @@ function save_profile_image($id, $uploadmanager, $dir='user') {
     $image->type   = $imageinfo[2];
 
     switch ($image->type) {
-        case 1: 
+        case IMAGETYPE_GIF:
             if (function_exists('ImageCreateFromGIF')) {
                 $im = ImageCreateFromGIF($originalfile); 
             } else {
@@ -142,7 +177,7 @@ function save_profile_image($id, $uploadmanager, $dir='user') {
                 return false;
             }
             break;
-        case 2: 
+        case IMAGETYPE_JPEG:
             if (function_exists('ImageCreateFromJPEG')) {
                 $im = ImageCreateFromJPEG($originalfile); 
             } else {
@@ -151,7 +186,7 @@ function save_profile_image($id, $uploadmanager, $dir='user') {
                 return false;
             }
             break;
-        case 3:
+        case IMAGETYPE_PNG:
             if (function_exists('ImageCreateFromPNG')) {
                 $im = ImageCreateFromPNG($originalfile); 
             } else {
diff --git a/lib/moodlelib.php b/lib/moodlelib.php
index 8ddb916..83f9377 100644
--- a/lib/moodlelib.php
+++ b/lib/moodlelib.php
@@ -6546,7 +6546,9 @@ function unzip_file ($zipfile, $destination = '', $showstatus = true) {
         if (!$list = $archive->extract(PCLZIP_OPT_PATH, $destpath,
                                        PCLZIP_CB_PRE_EXTRACT, 'unzip_cleanfilename',
                                        PCLZIP_OPT_EXTRACT_DIR_RESTRICTION, $destpath)) {
-            notice($archive->errorInfo(true));
+            if($showstatus) {
+                notice($archive->errorInfo(true));
+            }
             return false;
         }
 
