[TikiWiki-commits] [Git][tikiwiki/tiki][cherry-pick-7d5b3197] 2 commits: [FIX] ItemLink: Enhance multi-value selection handling in item link selector

"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]> Thu, 02 Jul 2026 13:32:51 +0000
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a4668836436d_38d81da060870@gitlab-sidekiq-low-urgency-cpu-bound-v2-65d6798dbd-m7q59.mail>

Elifeleti Mukisa Dan pushed to branch cherry-pick-7d5b3197 at Tiki Wiki CMS Groupware / Tiki


Commits:
812a2822 by Steven ngesera at 2026-07-02T12:30:21+00:00
[FIX] ItemLink: Enhance multi-value selection handling in item link selector
---
* [FIX] ItemLink: Add option to force object selector for multi-value selection

(cherry picked from commit 48e67b24ae9683b6a5d979485b9bc164ce1e524e)

* [FIX] ItemLink: Enhance multi-value selection handling in item link selector

(cherry picked from commit 7d2d5055799cc57e68235d556bbcebda081be6aa)

See merge request tikiwiki/tiki!10638

- - - - -
c76c8605 by Elifeleti Mukisa Dan at 2026-07-02T13:32:30+00:00
[FIX] User Avatars: uploading custom user profile picture leading to error 500 (WSoD)
---
* [FIX] Avatar upload was let to error 500 WSoD

* This reverts commit 165e29644df751b4193569f53c1c6fa438517943.

* [ENH] Implemented a hardening patch that closes the dangerous base-image ingestion path

See merge request tikiwiki/tiki!10631


(cherry picked from commit 7d5b3197608b75a90aaa5afca38d588b8f62b3f9)

26529287 [ENH] Implemented a hardening patch that closes the dangerous base-image ingestion path
019ad59e This reverts commit 165e29644df751b4193569f53c1c6fa438517943.
3aa94760 [FIX] Avatar upload was let to error 500 WSoD

Co-authored-by: Elifeleti Mukisa Dan <[email protected]>
- - - - -


8 changed files:

- doc/devtools/process_user_logins.php
- lib/avatarlib.php
- lib/core/Services/User/Controller.php
- lib/core/Tracker/Field/ItemLink.php
- lib/socialnetworkslib.php
- lib/trackers/trackerlib.php
- templates/trackerinput/itemlink_selector.tpl
- tiki-pick_avatar.php


Changes:

=====================================
doc/devtools/process_user_logins.php
=====================================
@@ -138,7 +138,7 @@ function processUsers(): void
                         if (strlen($name) > 80) {
                             $name = substr($name, 0, 80);
                         }
-                        $avatarlib->set_avatar_from_url($wrapper->getReadableFile(), $login, $name);
+                        $avatarlib->setAvatarFromFile($wrapper->getReadableFile(), $login, $name);
 
                         echo " done\n";
                     }


=====================================
lib/avatarlib.php
=====================================
@@ -22,16 +22,54 @@ if (str_contains($_SERVER['SCRIPT_NAME'], basename(__FILE__))) {
 class AvatarLib extends TikiLib
 {
     /**
-     * sets the avatar from a given image file's URL
+     * sets the avatar from a given remote image file's URL
      *
-     * @param string $url        location of the file
+     * @param string $url        location of the remote file
      * @param string $userwatch  user the avatar is for
      * @param string $name       original name of the file
      *
      * @throws Exception
      */
-
     final public function set_avatar_from_url(string $url, string $userwatch = '', string $name = ''): void
+    {
+        // Validate URL to prevent SSRF via user-supplied avatar URLs
+        $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+        if (! $ssrf->isUrlAllowed($url)) {
+            throw new \Exception($this->getDisallowedAvatarUrlMessage($url));
+        }
+
+        $this->setAvatarFromSource($url, $userwatch, $name, true);
+    }
+
+    /**
+     * sets the avatar from a local image file path
+     *
+     * @param string $file       local file path
+     * @param string $userwatch  user the avatar is for
+     * @param string $name       original name of the file
+     *
+     * @throws Exception
+     */
+    final public function setAvatarFromFile(string $file, string $userwatch = '', string $name = ''): void
+    {
+        if (! is_file($file) || ! is_readable($file)) {
+            throw new \Exception(tr('Avatar file is not readable'));
+        }
+
+        $this->setAvatarFromSource($file, $userwatch, $name, false);
+    }
+
+    private function getDisallowedAvatarUrlMessage(string $url): string
+    {
+        $host = parse_url($url, PHP_URL_HOST);
+        if ($host && Perms::get()->admin) {
+            return tr('Avatar URL host "%0" is not allowed. Review the SSRF whitelist in Security Admin.', $host);
+        }
+
+        return tr('Avatar URL is not allowed. Ask a site administrator to review the SSRF whitelist.');
+    }
+
+    private function setAvatarFromSource(string $source, string $userwatch, string $name, bool $isRemoteUrl): void
     {
         global $user, $prefs;
 
@@ -47,23 +85,26 @@ class AvatarLib extends TikiLib
             $userwatch = $user;
         }
 
-        // Validate URL to prevent SSRF via user-supplied avatar URLs
-        $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
-        if (! $ssrf->isUrlAllowed($url)) {
-            throw new \Exception('Avatar URL is not allowed');
+        $data = @file_get_contents($source);
+        if ($data === false) {
+            throw new \Exception($isRemoteUrl ? tr('Avatar URL could not be read.') : tr('Avatar file could not be read.'));
         }
 
-        $data = file_get_contents($url);
-        list($iwidth, $iheight, $itype, $iattr) = getimagesize($url);
+        $imageInfo = @getimagesize($source);
+        if ($imageInfo === false) {
+            throw new \Exception($isRemoteUrl ? tr('Avatar URL does not point to a valid image.') : tr('Avatar file is not a valid image.'));
+        }
+        list($iwidth, $iheight, $itype, $iattr) = $imageInfo;
         $itype = image_type_to_mime_type($itype);
 
         // Get proper file size of image
-        $imgdata = get_headers($url, true);
-        if (isset($imgdata['Content-Length'])) {
-            # Return file size
-            $size = (int)$imgdata['Content-Length'];
-        } else {
-            $size = strlen($data);
+        $size = strlen($data);
+        if ($isRemoteUrl) {
+            $imgdata = @get_headers($source, true);
+            if (isset($imgdata['Content-Length'])) {
+                # Return file size
+                $size = (int)$imgdata['Content-Length'];
+            }
         }
 
         // Store full-size file gallery image if that is required
@@ -85,6 +126,9 @@ class AvatarLib extends TikiLib
         } else {
             if (function_exists('imagecreatefromstring') && (! str_contains($itype, 'gif'))) {
                 $img = imagecreatefromstring($data);
+                if ($img === false) {
+                    throw new \Exception(tr('Avatar image could not be processed.'));
+                }
                 $size_x = imagesx($img);
                 $size_y = imagesy($img);
                 /* if the square crop is set, crop the image before resizing */
@@ -94,6 +138,9 @@ class AvatarLib extends TikiLib
                     $offset_y = ($size_y - $crop_size) / 2;
                     $crop_array = ['x' => $offset_x , 'y' => $offset_y, 'width' => $crop_size, 'height' => $crop_size];
                     $img = imagecrop($img, $crop_array);
+                    if ($img === false) {
+                        throw new \Exception(tr('Avatar image could not be cropped.'));
+                    }
                     $size_x = $size_y = $crop_size;
                 }
                 if ($size_x > $size_y) {
@@ -109,24 +156,43 @@ class AvatarLib extends TikiLib
                 if ($ty > $size_y) {
                     $ty = $size_y;
                 }
-                if (chkgd2()) {
-                    $t = imagecreatetruecolor($tw, $ty);
-                    // trick to have a transparent background for png instead of black
-                    imagesavealpha($t, true);
-                    $trans_colour = imagecolorallocatealpha($t, 0, 0, 0, 127);
-                    imagefill($t, 0, 0, $trans_colour);
-                    imagecopyresampled($t, $img, 0, 0, 0, 0, $tw, $ty, $size_x, $size_y);
-                } else {
+                if (! chkgd2()) {
                     // TODO ImageGalleryRemoval23.x - replace imagick if no GD
+                    throw new \Exception(tr('Avatar image could not be resized because GD is not available.'));
+                }
+                $t = imagecreatetruecolor($tw, $ty);
+                if ($t === false) {
+                    throw new \Exception(tr('Avatar image could not be resized.'));
+                }
+                // trick to have a transparent background for png instead of black
+                imagesavealpha($t, true);
+                $trans_colour = imagecolorallocatealpha($t, 0, 0, 0, 127);
+                imagefill($t, 0, 0, $trans_colour);
+                if (! imagecopyresampled($t, $img, 0, 0, 0, 0, $tw, $ty, $size_x, $size_y)) {
+                    throw new \Exception(tr('Avatar image could not be resized.'));
                 }
                 // CHECK IF THIS TEMP IS WRITEABLE OR CHANGE THE PATH TO A WRITEABLE DIRECTORY
                 $tmpfname = tempnam($prefs['tmpDir'], "TMPIMG");
-                imagepng($t, $tmpfname);
+                if ($tmpfname === false || ! imagepng($t, $tmpfname)) {
+                    throw new \Exception(tr('Avatar image could not be saved to a temporary file.'));
+                }
                 // Now read the information
                 $fp = fopen($tmpfname, "rb");
-                $t_data = fread($fp, filesize($tmpfname));
+                if ($fp === false) {
+                    throw new \Exception(tr('Avatar image temporary file could not be read.'));
+                }
+                $tmpSize = filesize($tmpfname);
+                if ($tmpSize === false) {
+                    fclose($fp);
+                    unlink($tmpfname);
+                    throw new \Exception(tr('Avatar image temporary file could not be read.'));
+                }
+                $t_data = fread($fp, $tmpSize);
                 fclose($fp);
                 unlink($tmpfname);
+                if ($t_data === false) {
+                    throw new \Exception(tr('Avatar image temporary file could not be read.'));
+                }
                 $t_type = 'image/png';
                 $userprefslib->set_user_avatar($userwatch, 'u', '', $name, $size, $t_type, $t_data);
             } else {


=====================================
lib/core/Services/User/Controller.php
=====================================
@@ -1107,11 +1107,22 @@ class Services_User_Controller
                 throw new Services_Exception($errormsg, 400);
             }
             $name = $_FILES['userfile']['name'];
+            $filegallib = TikiLib::lib('filegal');
+            try {
+                $filegallib->assertUploadedFileIsSafe($_FILES['userfile']['tmp_name'], $_FILES['userfile']['name']);
+            } catch (Exception $e) {
+                throw new Services_Exception($e->getMessage(), 403);
+            }
+
             /**
              * @var $avatarlib AvatarLib
              */
             $avatarlib = TikiLib::lib('avatar');
-            $avatarlib->set_avatar_from_url($_FILES['userfile']['tmp_name'], $userwatch, $name);
+            try {
+                $avatarlib->setAvatarFromFile($_FILES['userfile']['tmp_name'], $userwatch, $name);
+            } catch (Exception $e) {
+                throw new Services_Exception($e->getMessage(), 400);
+            }
             return true;
         } else {
             return [


=====================================
lib/core/Tracker/Field/ItemLink.php
=====================================
@@ -284,6 +284,16 @@ class Tracker_Field_ItemLink extends \Tracker\Field\AbstractItemField implements
                         ],
                         'legacy_index' => 12,
                     ],
+                    'forceObjectSelector' => [
+                        'name' => tr('Force object selector'),
+                        'description' => tr('Override the automatic widget selection. 1 forces the object selector (search panel with checkboxes for multi-select). 2 forces the dropdown. 0 lets the system decide (default behavior).'),
+                        'filter' => 'int',
+                        'options' => [
+                            0 => tr('System default'),
+                            1 => tr('Yes'),
+                            2 => tr('No'),
+                        ],
+                    ],
                     'indexRemote' => [
                         'name' => tr('Index remote fields'),
                         'description' => tr('Index one or multiple fields from the master tracker along with the child, separated by |'),
@@ -344,8 +354,16 @@ class Tracker_Field_ItemLink extends \Tracker\Field\AbstractItemField implements
             'value' => $value,
         ];
 
-        if ($this->canHaveMultipleValues() && ! is_array($data['value'])) {
-            $data['value'] = explode(',', $data['value']);
+        if ($this->canHaveMultipleValues()) {
+            if (isset($requestData[$string_id])) {
+                $parts = is_array($data['value']) ? array_values($data['value']) : preg_split('/[\s,]+/', $data['value'], -1, PREG_SPLIT_NO_EMPTY);
+                if (array_filter($parts, fn($v) => str_starts_with($v, 'trackeritem:'))) {
+                    $parts = array_values(array_filter(array_map(fn($v) => preg_replace('/^trackeritem:/', '', trim($v)), $parts)));
+                }
+                $data['value'] = $parts;
+            } elseif (! is_array($data['value'])) {
+                $data['value'] = explode(',', $data['value']);
+            }
         }
 
         return $data;
@@ -383,6 +401,15 @@ class Tracker_Field_ItemLink extends \Tracker\Field\AbstractItemField implements
             return false;
         }
 
+        $force = (int) $this->trackerField->getOption('forceObjectSelector');
+        if ($force === 1) {
+            return true;
+        }
+        if ($force === 2) {
+            return false;
+        }
+
+        // Default: preserve existing behaviour — multiselect stays on dropdown
         if ($this->canHaveMultipleValues()) {
             return false;
         }
@@ -427,13 +454,14 @@ class Tracker_Field_ItemLink extends \Tracker\Field\AbstractItemField implements
             $sort = TikiLib::lib('trk')->get_default_sort_order($trackerId, true, true);
 
             $template = $this->renderTemplate('trackerinput/itemlink_selector.tpl', $context, [
-                'placeholder' => $placeholder,
-                'status' => $status,
-                'selector_value' => $value,
-                'selector_id' => 'item' . $this->getItemId() . $this->getInsertId(),
-                'format' => $format,
-                'createTrackerItems' => $trackerPerms->create_tracker_items,
-                'sort' => $sort,
+                'placeholder'          => $placeholder,
+                'status'               => $status,
+                'selector_value'       => $value,
+                'selector_id'          => 'item' . $this->getItemId() . $this->getInsertId(),
+                'format'               => $format,
+                'createTrackerItems'   => $trackerPerms->create_tracker_items,
+                'sort'                 => $sort,
+                'selectMultipleValues' => (bool) $this->trackerField->getOption('selectMultipleValues'),
             ]);
 
             return $template;


=====================================
lib/socialnetworkslib.php
=====================================
@@ -356,7 +356,11 @@ class SocialNetworksLib extends LogsLib
         if ($prefs['feature_userPreferences'] == 'y') {
             $fb_avatar = json_decode($this->facebookGraph('', 'me/picture', ['type' => 'square', 'width' => '480', 'redirect' => '0','access_token' => $access_token], false, 'GET'));
             $avatarlib = TikiLib::lib('avatar');
-            $avatarlib->set_avatar_from_url($fb_avatar->data->url, $user);
+            try {
+                $avatarlib->set_avatar_from_url($fb_avatar->data->url, $user);
+            } catch (Exception $e) {
+                Feedback::error($e->getMessage());
+            }
         }
 
         return $user;
@@ -550,7 +554,11 @@ class SocialNetworksLib extends LogsLib
                     }
                     if ($displayImage) {
                         $avatarlib = TikiLib::lib('avatar');
-                        $avatarlib->set_avatar_from_url($displayImage, $user);
+                        try {
+                            $avatarlib->set_avatar_from_url($displayImage, $user);
+                        } catch (Exception $e) {
+                            Feedback::error($e->getMessage());
+                        }
                     }
                 }
             } else {


=====================================
lib/trackers/trackerlib.php
=====================================
@@ -2236,7 +2236,7 @@ class TrackerLib extends TikiLib
                             try {
                                 $filegallib->assertUploadedFileIsSafe($_FILES[$filekey]['tmp_name'], $_FILES[$filekey]['name']);
                                 $avatarlib = TikiLib::lib('avatar');
-                                $avatarlib->set_avatar_from_url($_FILES[$filekey]['tmp_name'], $trackersync_user, $_FILES[$filekey]['name']);
+                                $avatarlib->setAvatarFromFile($_FILES[$filekey]['tmp_name'], $trackersync_user, $_FILES[$filekey]['name']);
                             } catch (Exception $e) {
                                 Feedback::error($e->getMessage());
                             }


=====================================
templates/trackerinput/itemlink_selector.tpl
=====================================
@@ -1,5 +1,9 @@
 <div class="item-link">
-    {object_selector _id=$data.selector_id _simplevalue=$field.value _simplename=$field.ins_id _placeholder=$data.placeholder type="trackeritem" tracker_id=$field.options_map.trackerId tracker_status=$data.status _format=$data.format _sort=$data.sort}
+    {if !empty($data.selectMultipleValues)}
+        {object_selector_multi _id=$data.selector_id _name=$field.ins_id _placeholder=$data.placeholder _format=$data.format _sort=$data.sort type="trackeritem" tracker_id=$field.options_map.trackerId tracker_status=$data.status}
+    {else}
+        {object_selector _id=$data.selector_id _simplevalue=$field.value _simplename=$field.ins_id _placeholder=$data.placeholder type="trackeritem" tracker_id=$field.options_map.trackerId tracker_status=$data.status _format=$data.format _sort=$data.sort}
+    {/if}
     {if $field.options_map.addItems and $data.createTrackerItems}
         <a class="btn btn-primary insert-tracker-item" href="{service controller=tracker action=insert_item trackerId=$field.options_map.trackerId}" role="button">{tr}{$field.options_map.addItems|escape}{/tr}</a>
         {jq}


=====================================
tiki-pick_avatar.php
=====================================
@@ -56,15 +56,19 @@ if (isset($_FILES['userfile1'])) {
         }
 
         $avatarlib = TikiLib::lib('avatar');
-        $avatarlib->set_avatar_from_url($_FILES['userfile1']['tmp_name'], $userwatch, $name);
+        try {
+            $avatarlib->setAvatarFromFile($_FILES['userfile1']['tmp_name'], $userwatch, $name);
 
-        /* redirect to prevent re-submit on page reload */
-        if ($tiki_p_admin == 'y' && $user !== $userwatch) {
-            header('Location: tiki-pick_avatar.php?view_user=' . $userwatch);
-        } else {
-            header('Location: tiki-pick_avatar.php');
+            /* redirect to prevent re-submit on page reload */
+            if ($tiki_p_admin == 'y' && $user !== $userwatch) {
+                header('Location: tiki-pick_avatar.php?view_user=' . $userwatch);
+            } else {
+                header('Location: tiki-pick_avatar.php');
+            }
+            exit;
+        } catch (Exception $e) {
+            Feedback::error($e->getMessage());
         }
-        exit;
     } else {
         Feedback::error($tikilib->uploaded_file_error($_FILES['userfile1']['error']));
     }



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/compare/f95fd0839452782677ac25a3d8b8b9fd369960bf...c76c860585365c88e77a1613a8efcc6cd5bf4ca0

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/compare/f95fd0839452782677ac25a3d8b8b9fd369960bf...c76c860585365c88e77a1613a8efcc6cd5bf4ca0
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs