/** * Performs the actual merging, using edited input from UI. * @param type $data - Contains pairs (value, name) from edit form in addition to the following fields: * * mergePosts - posts to be merged * * destnodeid - target post * * destauthorid - author to be used * * contenttype - target contenttype */ public function mergePosts($input) { $this->inlinemodAuthCheck(); $cleaner = vB::getCleaner(); $data = array(); foreach ($input as $i) { $name = $cleaner->clean($i['name'], vB_Cleaner::TYPE_NOHTML); //mostly the data is either integer or string, although the possibility exists of a //'url_image' switch ($name) { case 'nodeid': case 'url_nopreview': case 'nodeuserid': case 'filedataid': case 'destauthorid': case 'destnodeid': $value = $cleaner->clean($i['value'], vB_Cleaner::TYPE_UINT); break; case 'title': case 'text': case 'reason': $value = $cleaner->clean($i['value'], vB_Cleaner::TYPE_STR); break; case 'url_title': case 'authorname': case 'url': case 'url_image': case 'url_meta': $value = $cleaner->clean($i['value'], vB_Cleaner::TYPE_NOHTML); break; case 'mergePosts': if (!is_array($i['value'])) { $i['value'] = explode(',', $i['value']); } $value = $cleaner->clean($i['value'], vB_Cleaner::TYPE_ARRAY_UINT); break; case 'filedataid[]': //The filedata records are passed as //input[xx][name] filedataid[] //input[xx][value] <integer> $value = $cleaner->clean($i['value'], vB_Cleaner::TYPE_UINT); if (!isset($data['filedataid'])) { $data['filedataid'] = array(); } if (!isset($data['filedataid'][$value])) { $data['filedataid'][$value] = ''; } continue; default: //The title records are passed as //input[xx][name] title_<filedataid> //input[xx][value] <title> if (empty($name)) { continue; } if (substr($name, 0, 6) == 'title_') { $filedataid = substr($name, 6); $filedataid = $cleaner->clean($filedataid, vB_Cleaner::TYPE_UINT); if ($filedataid) { if (!isset($data['filedataid'])) { $data['filedataid'] = array(); } $data['filedataid'][$filedataid] = $cleaner->clean($i['value'], vB_Cleaner::TYPE_NOHTML); } continue; } else { if (preg_match('#^videoitems\\[([\\d]+)#', $name, $matches)) { if (!isset($data['videoitems'])) { $data['videoitems'] = array(); } $videoitems[] = array('videoitemid' => intval($matches[1]), 'url' => $i['value']); } else { if (preg_match('^videoitems\\[new^', $name, $matches)) { if (!isset($data['videoitems'])) { $data['videoitems'] = array(); } foreach ($matches as $video) { $data['videoitems'][] = array('url' => $video['url']); } } } } continue; } if (isset($data[$name])) { if (!is_array($data[$name])) { $data[$name] = array($data[$name]); } $data[$name][] = $value; } else { $data[$name] = $value; } } if (empty($data['mergePosts'])) { throw new vB_Exception_Api('please_select_at_least_one_post'); } // check that the user has permission $nodesToCheck = $data['mergePosts']; $nodesToCheck[] = $data['destnodeid']; foreach ($nodesToCheck as $key => $nodeid) { // this is here just in case for some reason, a nodeid is 0. Shouldn't happen, but // I don't want getChannelPermission to go bonkers from it. if (empty($nodeid)) { unset($nodesToCheck[$key]); continue; } if (!vB::getUserContext()->getChannelPermission('moderatorpermissions', 'canmanagethreads', $nodeid)) { // perhaps we could generate a list of unmergeable nodes and return a warning instead, but // I don't think there's a real use case where a moderator can manage only *some* of the // nodes they're trying to merge. I think that would require multiple channels being involved, and // we don't have a UI for that so I can't test it. As such I'm just going to throw an exception if // *any* of the nodes fail the check. throw new vB_Exception_Api('no_permission'); } } // validate that selected nodes can be merged $mergeInfo = $this->validateMergePosts($data['mergePosts']); if (isset($mergeInfo['error'])) { throw new vB_Exception_Api($mergeInfo['error']); } // validate form fields if (empty($data['destnodeid']) || !array_key_exists($data['destnodeid'], $mergeInfo['destnodes'])) { throw new vB_Exception_Api('invalid_data'); } if (empty($data['destauthorid']) || !array_key_exists($data['destauthorid'], $mergeInfo['destauthors'])) { throw new vB_Exception_Api('invalid_data'); } $destnode = $this->library->getNodeFullContent($data['destnodeid']); $destnode = array_pop($destnode); if ($destnode['starter'] != $destnode['nodeid'] and $destnode['starter'] != $destnode['parentid']) { if (isset($data['tags'])) { unset($data['tags']); } } $type = vB_Types::instance()->getContentTypeClass($destnode['contenttypeid']); $response = vB_Library::instance("content_{$type}")->mergeContent($data); if ($response) { $sources = array_diff($data['mergePosts'], array($data['destnodeid'])); $origDestnode = $destnode; if (!empty($destnode['rawtext'])) { $origRawText = $destnode['rawtext']; } else { if (!empty($destnode['content']['rawtext'])) { $origRawText = $destnode['content']['rawtext']; } else { $origRawText = ''; } } $destnode = $this->getNode($data['destnodeid']); if (!empty($destnode['rawtext'])) { $rawText = $destnode['rawtext']; } else { if (!empty($destnode['content']['rawtext'])) { $rawText = $destnode['content']['rawtext']; } else { $rawText = ''; } } $destnode = $this->getNode($data['destnodeid']); $loginfo = array('nodeid' => $destnode['nodeid'], 'nodetitle' => $destnode['title'], 'nodeusername' => $destnode['authorname'], 'nodeuserid' => $destnode['userid']); // move children to target node $children = vB::getDbAssertor()->assertQuery('vBForum:closure', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT, 'parent' => $sources, 'depth' => 1)); $childrenIds = array(); foreach ($children as $child) { $childrenIds[] = $child['child']; } if (!empty($childrenIds)) { $this->moveNodes($childrenIds, $data['destnodeid'], false, false, false); } // remove merged nodes $this->deleteNodes($sources, true, null, false); // Dont log the deletes $loginfo['action'] = array('merged_nodes' => implode(',', $sources)); $vboptions = vB::getDatastore()->getValue('options'); if (vB_Api::instanceInternal('user')->hasPermissions('genericoptions', 'showeditedby') and $destnode['publishdate'] > 0 and $destnode['publishdate'] < vB::getRequest()->getTimeNow() - $vboptions['noeditedbytime'] * 60 or !empty($data['reason'])) { $userinfo = vB::getCurrentSession()->fetch_userinfo(); if ($vboptions['postedithistory']) { $record = vB::getDbAssertor()->getRow('vBForum:postedithistory', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT, 'original' => 1, 'nodeid' => $destnode['nodeid'])); // insert original post on first edit if (empty($record)) { vB::getDbAssertor()->assertQuery('vBForum:postedithistory', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_INSERT, 'nodeid' => $origDestnode['nodeid'], 'userid' => $origDestnode['userid'], 'username' => $origDestnode['authorname'], 'dateline' => $origDestnode['publishdate'], 'pagetext' => $origRawText, 'original' => 1)); } // insert the new version vB::getDbAssertor()->assertQuery('vBForum:postedithistory', array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_INSERT, 'nodeid' => $destnode['nodeid'], 'userid' => $userinfo['userid'], 'username' => $userinfo['username'], 'dateline' => vB::getRequest()->getTimeNow(), 'reason' => isset($data['reason']) ? vB5_String::htmlSpecialCharsUni($data['reason']) : '', 'pagetext' => $rawText)); } vB::getDbAssertor()->assertQuery('editlog_replacerecord', array('nodeid' => $destnode['nodeid'], 'userid' => $userinfo['userid'], 'username' => $userinfo['username'], 'timenow' => vB::getRequest()->getTimeNow(), 'reason' => isset($data['reason']) ? vB5_String::htmlSpecialCharsUni($data['reason']) : '', 'hashistory' => intval($vboptions['postedithistory']))); } vB_Library_Admin::logModeratorAction($loginfo, 'node_merged_by_x'); return true; } }