helper('Tag', 'Post'); $search = trim($this->params()->search) ?: ""; $q = [ 'keywords' => [] ]; if ($search) { foreach (explode(' ', $search) as $s) { if (preg_match('/^(.+?):(.*)/', $s, $m)) { $search_type = $m[1]; $param = $m[2]; if ($search_type == "user") { $q['user'] = $param; } elseif ($search_type == "change") { $q['change'] = (int)$param; } elseif ($search_type == "type") { $q['type'] = $param; } elseif ($search_type == "id") { $q['id'] = (int)$param; } elseif ($search_type == "field") { # 'type' must also be set for this to be used. $q['field'] = $param; } else { # pool'123' $q['type'] = $search_type; $q['id'] = (int)$param; } } else { $q['keywords'][] = $s; } } } $inflector = Rails::services()->get('inflector'); if (!empty($q['type'])) { $q['type'] = $inflector->pluralize($q['type']); } if (!empty($q['inner_type'])) { $q['inner_type'] = $inflector->pluralize($q['inner_type']); } # If notes'id' has been specified, search using the inner key in history_changes # rather than the grouping table in histories. We don't expose this in general. # Searching based on hc.table_name without specifying an ID is slow, and the # details here shouldn't be visible anyway. if (array_key_exists('type', $q) and array_key_exists('id', $q) and $q['type'] == "notes") { $q['inner_type'] = $q['type']; $q['remote_id'] = $q['id']; unset($q['type']); unset($q['id']); } $query = History::none(); $hc_conds = []; $hc_cond_params = []; if (!empty($q['user'])) { $user = User::where('name', $q['user'])->first(); if ($user) { $query->where("histories.user_id = ?", $user->id); } else { $query->where("false"); } } if (!empty($q['id'])) { $query->where("group_by_id = ?", $q['id']); } if (!empty($q['type'])) { $query->where("group_by_table = ?", $q['type']); } if (!empty($q['change'])) { $query->where("histories.id = ?", $q['change']); } if (!empty($q['inner_type'])) { $q['inner_type'] = $inflector->pluralize($q['inner_type']); $hc_conds[] = "hc.table_name = ?"; $hc_cond_params[] = $q['inner_type']; } if (!empty($q['remote_id'])) { $hc_conds[] = "hc.remote_id = ?"; $hc_cond_params[] = $q['remote_id']; } if ($q['keywords']) { $hc_conds[] = 'hc.value LIKE ?'; $hc_cond_params[] = '%' . implode('%', $q['keywords']) . '%'; } if (!empty($q['field']) and !empty($q['type'])) { # Look up a particular field change, eg. "type'posts' field'rating'". # XXX: The WHERE id IN (SELECT id...) used to implement this is slow when we don't have # anything } else { filtering the results. $field = $q['field']; $table = $q['type']; # For convenience: if ($field == "tags") { $field = "cached_tags"; } # Look up the named class. if (!Versioned::is_versioned_class($cls)) { $query->where("false"); } else { $hc_conds[] = "hc.column_name = ?"; $hc_cond_params[] = $field; # A changes that has no previous value is the initial value for that object. Don't show # these changes unless they're different from the default for that field. list ($default_value, $has_default) = $cls::versioning()->get_versioned_default($field); if ($has_default) { $hc_conds[] = "(hc.previous_id IS NOT NULL OR value <> ?)"; $hc_cond_params[] = $default_value; } } } if ($hc_conds) { array_unshift($hc_cond_params, 'histories.id IN (SELECT history_id FROM history_changes hc JOIN histories h ON (hc.history_id = h.id) WHERE ' . implode(" AND ", $hc_conds) . ')'); call_user_func_array([$query, 'where'], $hc_cond_params); } if (!empty($q['type']) and empty($q['change'])) { $this->type = $q['type']; } else { $this->type = "all"; } # 'specific_history' => showing only one history # 'specific_table' => showing changes only for a particular table # 'show_all_tags' => don't omit post tags that didn't change $this->options = [ 'show_all_tags' => $this->params()->show_all_tags == "1", 'specific_object' => (!empty($q['type']) and !empty($q['id'])), 'specific_history' => !empty($q['change']), ]; $this->options['show_name'] = false; if ($this->type != "all") { $cn = $inflector->classify($this->type); try { if (Versioned::is_versioned_class($cls) && class_exists($cn)) { $obj = new $cn(); if (method_exists($obj, "pretty_name")) $this->options['show_name'] = true; } } catch (Rails\Loader\Exception\ExceptionInterface $e) { } } $this->changes = $query->order("histories.id DESC") ->select('*') ->paginate($this->page_number(), 20); # If we're searching for a specific change, force the display to the # type of the change we found. if (!empty($q['change']) && $this->changes->any()) { $this->type = $inflector->pluralize($this->changes[0]->group_by_table); } $this->render(['action' => 'index']); } public function undo() { $ids = explode(',', $this->params()->id); $this->changes = HistoryChange::emptyCollection(); foreach ($ids as $id) $this->changes[] = HistoryChange::where("id = ?", $id)->first(); $histories = []; $total_histories = 0; foreach ($this->changes as $change) { if (isset($histories[$change->history_id])) continue; $histories[$change->history_id] = true; $total_histories += 1; } if ($total_histories > 1 && !$this->current_user->is_privileged_or_higher()) { $this->respond_to_error("Only privileged users can undo more than one change at once", ['status' => 403]); return; } $errors = []; History::undo($this->changes, $this->current_user, $this->params()->redo == "1", $errors); $error_texts = []; $successful = 0; $failed = 0; foreach ($this->changes as $change) { $objectHash = spl_object_hash($change); if (empty($errors[$objectHash])) { $successful += 1; continue; } $failed += 1; switch ($errors[$objectHash]) { case 'denied': $error_texts[] = "Some changes were not made because you do not have access to make them."; break; } } $error_texts = array_unique($error_texts); $this->respond_to_success("Changes made.", ['action' => "index"], ['api' => ['successful' => $successful, 'failed' => $failed, 'errors' => $error_texts]]); } }