[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