[ 'text' => 'text', 'has_option' => false, 'is_value_multiple' => false, ], self::TYPE_TEXTAREA => [ 'text' => 'textarea', 'has_option' => false, 'is_value_multiple' => false, ], self::TYPE_RADIO => [ 'text' => 'radio', 'has_option' => true, 'is_value_multiple' => false, ], self::TYPE_CHECKBOX => [ 'text' => 'checkbox', 'has_option' => true, 'is_value_multiple' => true, ], self::TYPE_SELECT => [ 'text' => 'select', 'has_option' => true, 'is_value_multiple' => false, ], self::TYPE_IMAGE => [ 'text' => 'image', 'has_option' => false, 'is_value_multiple' => false, ], ]; private $preparedTorrentCustomFieldValues = []; public function getTypeHuman($type) { global $lang_fields; $map = [ self::TYPE_TEXT => $lang_fields['field_type_text'], self::TYPE_TEXTAREA => $lang_fields['field_type_textarea'], self::TYPE_RADIO => $lang_fields['field_type_radio'], self::TYPE_CHECKBOX => $lang_fields['field_type_checkbox'], self::TYPE_SELECT => $lang_fields['field_type_select'], self::TYPE_IMAGE => $lang_fields['field_type_image'], ]; return $map[$type] ?? ''; } public function getTypeRadioOptions() { $out = []; foreach (self::$types as $key => $value) { $out[$key] = sprintf('%s(%s)', $value['text'], $this->getTypeHuman($key)); } return $out; } public function radio($name, $options, $current = null) { $arr = []; foreach ($options as $value => $label) { $arr[] = sprintf( '', $name, $value, (string)$current === (string)$value ? ' checked' : '', $label ); } return implode('', $arr); } function buildFieldForm(array $row = []) { global $lang_fields, $lang_functions, $lang_catmanage; $trName = tr($lang_fields['col_name'] . '*', '  ' . $lang_fields['col_name_help'], 1, '', true); $trLabel = tr($lang_fields['col_label'] . '*', '', 1, '', true); $trType = tr($lang_fields['col_type'] . '*', $this->radio('type', $this->getTypeRadioOptions(), $row['type'] ?? null), 1, '', true); $trRequired = tr($lang_fields['col_required'] . '*', $this->radio('required', ['0' => $lang_functions['text_no'], '1' => $lang_functions['text_yes']], $row['required'] ?? null), 1, '', true); $trHelp = tr($lang_fields['col_help'], '', 1, '', true); $trOptions = tr($lang_fields['col_options'], '
' . $lang_fields['col_options_help'], 1, '', true); $trIsSingleRow = tr($lang_fields['col_is_single_row'] . '*', $this->radio('is_single_row', ['0' => $lang_functions['text_no'], '1' => $lang_functions['text_yes']], $row['is_single_row'] ?? null), 1, '', true); $trPriority = tr(nexus_trans('label.priority') . '*', '', 1, '', true); $trDisplay = tr($lang_fields['col_display'], '
' . $lang_catmanage['row_custom_field_display_help'], 1, '', true); $id = $row['id'] ?? 0; $form = <<

{$lang_fields['text_field']}

{$trName} {$trLabel} {$trType} {$trRequired} {$trHelp} {$trOptions} {$trIsSingleRow} {$trPriority} {$trDisplay}
HTML; return $form; } function buildFieldTable() { global $lang_fields, $lang_functions; $perPage = 10; $total = get_row_count('torrents_custom_fields'); list($paginationTop, $paginationBottom, $limit) = pager($perPage, $total, "?"); $sql = "select * from torrents_custom_fields order by priority desc $limit"; $res = sql_query($sql); $header = [ 'id' => $lang_fields['col_id'], 'name' => $lang_fields['col_name'], 'label' => $lang_fields['col_label'], 'type_text' => $lang_fields['col_type'], 'required_text' => $lang_fields['col_required'], 'is_single_row_text' => $lang_fields['col_is_single_row'], 'priority' => nexus_trans('label.priority'), 'action' => $lang_fields['col_action'], ]; $rows = []; while ($row = mysql_fetch_assoc($res)) { $row['required_text'] = $row['required'] ? $lang_functions['text_yes'] : $lang_functions['text_no']; $row['is_single_row_text'] = $row['is_single_row'] ? $lang_functions['text_yes'] : $lang_functions['text_no']; $row['type_text'] = sprintf('%s(%s)', $this->getTypeHuman($row['type']), $row['type']); $row['action'] = sprintf( "%s | %s", $row['id'], $lang_fields['js_sure_to_delete_this'], $lang_fields['text_delete'], $row['id'], $lang_fields['text_edit'] ); $rows[] = $row; } $head = <<{$lang_fields['field_management']}
{$lang_fields['text_add']}
HEAD; $table = $this->buildTable($header, $rows); return $head . $table . $paginationBottom; } public function save($data) { global $lang_functions, $lang_fields; $attributes = []; if (empty($data['name'])) { throw new \InvalidArgumentException("{$lang_fields['col_name']} {$lang_functions['text_required']}"); } if (!preg_match('/^\w+$/', $data['name'])) { throw new \InvalidArgumentException("{$lang_fields['col_name']} {$lang_functions['text_invalid']}"); } $attributes['name'] = $data['name']; if (empty($data['label'])) { throw new \InvalidArgumentException("{$lang_fields['col_label']} {$lang_functions['text_required']}"); } $attributes['label'] = $data['label']; if (empty($data['type'])) { throw new \InvalidArgumentException("{$lang_fields['col_type']} {$lang_functions['text_required']}"); } if (!isset(self::$types[$data['type']])) { throw new \InvalidArgumentException("{$lang_fields['col_type']} {$lang_functions['text_invalid']}"); } $attributes['type'] = $data['type']; if (!isset($data['required'])) { throw new \InvalidArgumentException("{$lang_fields['col_required']} {$lang_functions['text_required']}"); } if (!in_array($data['required'], ["0", "1"], true)) { throw new \InvalidArgumentException("{$lang_fields['col_name']} {$lang_functions['text_invalid']}"); } $attributes['required'] = $data['required']; if (!isset($data['is_single_row'])) { throw new \InvalidArgumentException("{$lang_fields['col_is_single_row']} {$lang_functions['text_required']}"); } if (!in_array($data['is_single_row'], ["0", "1"], true)) { throw new \InvalidArgumentException("{$lang_fields['col_is_single_row']} {$lang_functions['text_invalid']}"); } $attributes['is_single_row'] = $data['is_single_row']; $attributes['help'] = $data['help'] ?? ''; $attributes['options'] = trim($data['options'] ?? ''); $attributes['display'] = trim($data['display'] ?? ''); $attributes['priority'] = trim($data['priority'] ?? '0'); $now = date('Y-m-d H:i:s'); $attributes['updated_at'] = $now; $table = 'torrents_custom_fields'; if (!empty($data['id'])) { $result = NexusDB::update($table, $attributes, "id = " . sqlesc($data['id'])); } else { $attributes['created_at'] = $now; $result = NexusDB::insert($table, $attributes); } return $result; } protected function buildTable(array $header, array $rows) { $table = ''; foreach ($header as $key => $value) { $table .= sprintf('', $value); } $table .= ''; foreach ($rows as $row) { $table .= ''; foreach ($header as $headerKey => $headerValue) { $table .= sprintf('', $row[$headerKey] ?? ''); } $table .= ''; } $table .= '
%s
%s
'; return $table; } public function buildFieldCheckbox($name, $current = []) { $sql = 'select * from torrents_custom_fields'; $res = sql_query($sql); if (!is_array($current)) { $current = explode(',', $current); } $checkbox = ''; while ($row = mysql_fetch_assoc($res)) { $checkbox .= sprintf( '', $name, $row['id'], in_array($row['id'], $current) ? ' checked' : '', "{$row['name']}[{$row['label']}]" ); } $checkbox .= ''; return $checkbox; } public function renderOnUploadPage($torrentId = 0, $searchBoxId) { $searchBox = NexusDB::getOne('searchbox', "id = $searchBoxId"); if (empty($searchBox)) { throw new \RuntimeException("Invalid search box: $searchBoxId"); } $customValues = $this->listTorrentCustomField($torrentId, $searchBoxId); $sql = sprintf('select * from torrents_custom_fields where id in (%s) order by priority desc', $searchBox['custom_fields'] ?: 0); $res = sql_query($sql); $html = ''; while ($row = mysql_fetch_assoc($res)) { $name = "custom_fields[$searchBoxId][{$row['id']}]"; $currentValue = $customValues[$row['id']]['custom_field_value'] ?? ''; $requireText = ''; if ($row['required']) { $requireText = "*"; } $trLabel = $row['label'] . $requireText; $trRelation = "mode_$searchBoxId"; if ($row['type'] == self::TYPE_TEXT) { $html .= tr($trLabel, sprintf('', $name, $currentValue, '99%'), 1, $trRelation); } elseif ($row['type'] == self::TYPE_TEXTAREA) { $html .= tr($trLabel, sprintf('', $name, '99%', $currentValue), 1, $trRelation); } elseif ($row['type'] == self::TYPE_RADIO || $row['type'] == self::TYPE_CHECKBOX) { if ($row['type'] == self::TYPE_CHECKBOX) { $name .= '[]'; } $part = ""; foreach (preg_split('/[\r\n]+/', trim($row['options'])) as $option) { if (empty($option) || ($pos = strpos($option, '|')) === false) { continue; } $value = substr($option, 0, $pos); $label = substr($option, $pos + 1); $checked = ""; if ($row['type'] == self::TYPE_RADIO && (string)$currentValue === (string)$value) { $checked = " checked"; } if ($row['type'] == self::TYPE_CHECKBOX && in_array($value, (array)$currentValue)) { $checked = " checked"; } $part .= sprintf( '', $row['type'], $name, $value, $checked, $label ); } $html .= tr($trLabel, $part, 1, $trRelation); } elseif ($row['type'] == self::TYPE_SELECT) { $part = ''; $html .= tr($trLabel, $part, 1, $trRelation); } elseif ($row['type'] == self::TYPE_IMAGE) { $callbackFunc = "preview_custom_field_image_" . $row['id']; $iframeId = "iframe_$callbackFunc"; $inputId = "input_$callbackFunc"; $imgId = "attach" . $row['id']; $previewBoxId = "preview_$callbackFunc"; $y = ''; $y .= sprintf('', $inputId, $name, $currentValue, '99%'); $y .= '
'; if (!empty($currentValue)) { if (substr($currentValue, 0, 4) == 'http') { $y .= formatImg($currentValue, true, 700, 0, $imgId); } else { $y .= format_comment($currentValue); } } $y .= '
'; $y .= << function {$callbackFunc}(delkey, url) { var previewBox = $('$previewBoxId') var existsImg = $('$imgId') var input = $('$inputId') if (existsImg) { previewBox.removeChild(existsImg) input.value = '' } var img = document.createElement('img') img.src=url img.setAttribute('onload', 'Scale(this, 700, 0);') img.setAttribute('onclick', 'Preview(this);') input.value = '[attach]' + delkey + '[/attach]' img.id='$imgId' previewBox.appendChild(img) } JS; $html .= tr($trLabel, $y, 1, $trRelation, true); } } return $html; } public function listTorrentCustomField($torrentId, $searchBoxId) { //suppose torrentId is array $isArray = true; $torrentIdArr = $torrentId; if (!is_array($torrentId)) { $isArray = false; $torrentIdArr = [$torrentId]; } $torrentIdStr = implode(',', $torrentIdArr); $res = sql_query("select f.*, v.custom_field_value, v.torrent_id from torrents_custom_field_values v inner join torrents_custom_fields f on v.custom_field_id = f.id inner join searchbox box on box.id = $searchBoxId and find_in_set(f.id, box.custom_fields) where torrent_id in ($torrentIdStr) order by f.priority desc"); $values = []; $result = []; while ($row = mysql_fetch_assoc($res)) { $typeInfo = self::$types[$row['type']]; if ($typeInfo['has_option']) { $options = preg_split('/[\r\n]+/', trim($row['options'])); $optionsArr = []; foreach ($options as $option) { $pos = strpos($option, '|'); $value = substr($option, 0, $pos); $label = substr($option, $pos + 1); $optionsArr[$value] = $label; } $row['options'] = $optionsArr; } $result[$row['torrent_id']][$row['id']] = $row; if ($typeInfo['is_value_multiple']) { $values[$row['torrent_id']][$row['id']] = json_decode($row['custom_field_value'], true); } else { $values[$row['torrent_id']][$row['id']] = $row['custom_field_value']; } } foreach ($result as $tid => &$fields) { foreach ($fields as &$field) { $field['custom_field_value'] = $values[$tid][$field['id']]; } } return $isArray ? $result : ($result[$torrentId] ?? []); } public function renderOnTorrentDetailsPage($torrentId, $searchBoxId) { $displayName = get_searchbox_value($searchBoxId, 'custom_fields_display_name'); $customFields = $this->listTorrentCustomField($torrentId, $searchBoxId); $mixedRowContent = get_searchbox_value($searchBoxId, 'custom_fields_display'); $rowByRowHtml = ''; $shouldRenderMixRow = false; foreach ($customFields as $field) { if (empty($field['custom_field_value'])) { //No value, remove special tags $mixedRowContent = str_replace("<%{$field['name']}.label%>", '', $mixedRowContent); $mixedRowContent = str_replace("<%{$field['name']}.value%>", '', $mixedRowContent); continue; } $shouldRenderMixRow = true; $contentNotFormatted = $this->formatCustomFieldValue($field, false); $mixedRowContent = str_replace("<%{$field['name']}.label%>", $field['label'], $mixedRowContent); $mixedRowContent = str_replace("<%{$field['name']}.value%>", $contentNotFormatted, $mixedRowContent); if ($field['is_single_row']) { if (!empty($field['display'])) { $customFieldDisplay = $field['display']; $customFieldDisplay = str_replace("<%{$field['name']}.label%>", $field['label'], $customFieldDisplay); $customFieldDisplay = str_replace("<%{$field['name']}.value%>", $contentNotFormatted, $customFieldDisplay); $rowByRowHtml .= tr($field['label'], format_comment($customFieldDisplay, false), 1); } else { $contentFormatted = $this->formatCustomFieldValue($field, true); $rowByRowHtml .= tr($field['label'], $contentFormatted, 1); } } } $result = $rowByRowHtml; if ($shouldRenderMixRow && $mixedRowContent) { $result .= tr($displayName, format_comment($mixedRowContent), 1); } return $result; } protected function formatCustomFieldValue(array $customFieldWithValue, $doFormatComment = false): string { $result = ''; $fieldValue = $customFieldWithValue['custom_field_value']; switch ($customFieldWithValue['type']) { case self::TYPE_TEXT: case self::TYPE_TEXTAREA: $result .= $doFormatComment ? format_comment($fieldValue, false) : $fieldValue; break; case self::TYPE_IMAGE: if (substr($fieldValue, 0, 4) == 'http') { $result .= $doFormatComment ? formatImg($fieldValue, true, 700, 0, "attach{$customFieldWithValue['id']}") : $fieldValue; } else { $result .= $doFormatComment ? format_comment($fieldValue, false) : $fieldValue; } break; case self::TYPE_RADIO: case self::TYPE_CHECKBOX: case self::TYPE_SELECT; $fieldContent = []; foreach ((array)$fieldValue as $item) { $fieldContent[] = $customFieldWithValue['options'][$item] ?? ''; } $result .= implode(' ', $fieldContent); break; default: break; } return $result; } public function prepareTorrents(array $torrentIdArr) { $customFieldValues = $this->listTorrentCustomField($torrentIdArr); $result = []; foreach ($customFieldValues as $tid => &$customFields) { foreach ($customFields as &$field) { $field['custom_field_value_formatted'] = $this->formatCustomFieldValue($field); $result[$tid][$field['name']] = $field; } } $this->preparedTorrentCustomFieldValues = $result; } public function getPreparedTorrent($torrentId = null, $fieldName = null) { if (is_null($torrentId)) { return $this->preparedTorrentCustomFieldValues; } if (is_null($fieldName)) { return $this->preparedTorrentCustomFieldValues[$torrentId] ?? []; } return $this->preparedTorrentCustomFieldValues[$torrentId][$fieldName] ?? ''; } public function saveFieldValues($searchBoxId, $torrentId, array $data) { $searchBox = SearchBox::query()->findOrFail($searchBoxId); $enabledFields = TorrentCustomField::query()->find($searchBox->custom_fields); $insert = []; $now = now(); foreach ($enabledFields as $field) { if (empty($data[$field->id])) { if ($field->required) { // throw new \InvalidArgumentException(nexus_trans("nexus.require_argument", ['argument' => $field->label])); do_log("Field: {$field->label} required, but empty"); } continue; } $insert[] = [ 'torrent_id' => $torrentId, 'custom_field_id' => $field->id, 'custom_field_value' => is_array($data[$field->id]) ? json_encode($data[$field->id]) : $data[$field->id], 'created_at' => $now, 'updated_at' => $now, ]; } TorrentCustomFieldValue::query()->where('torrent_id', $torrentId)->delete(); TorrentCustomFieldValue::query()->insert($insert); } }