diff --git a/ext/source_history/main.php b/ext/source_history/main.php new file mode 100644 index 00000000..68d748de --- /dev/null +++ b/ext/source_history/main.php @@ -0,0 +1,376 @@ +set_default_int("history_limit", -1); + + // shimmie is being installed so call install to create the table. + if($config->get_int("ext_source_history_version") < 3) { + $this->install(); + } + } + + public function onAdminBuilding(AdminBuildingEvent $event) { + $this->theme->display_admin_block(); + } + + public function onPageRequest(PageRequestEvent $event) { + global $config, $page, $user; + + if($event->page_matches("source_history/revert")) { + // this is a request to revert to a previous version of the source + if($user->can("edit_image_tag")) { + if(isset($_POST['revert'])) { + $this->process_revert_request($_POST['revert']); + } + } + } + else if($event->page_matches("source_history/bulk_revert")) { + if($user->can("bulk_edit_image_tag") && $user->check_auth_token()) { + $this->process_bulk_revert_request(); + } + } + else if($event->page_matches("source_history/all")) { + $page_id = int_escape($event->get_arg(0)); + $this->theme->display_global_page($page, $this->get_global_source_history($page_id), $page_id); + } + else if($event->page_matches("source_history") && $event->count_args() == 1) { + // must be an attempt to view a source history + $image_id = int_escape($event->get_arg(0)); + $this->theme->display_history_page($page, $image_id, $this->get_source_history_from_id($image_id)); + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) { + $event->add_part(" +
+ +
+ ", 20); + } + + /* + // disk space is cheaper than manually rebuilding history, + // so let's default to -1 and the user can go advanced if + // they /really/ want to + public function onSetupBuilding(SetupBuildingEvent $event) { + $sb = new SetupBlock("Source History"); + $sb->add_label("Limit to "); + $sb->add_int_option("history_limit"); + $sb->add_label(" entires per image"); + $sb->add_label("
(-1 for unlimited)"); + $event->panel->add_block($sb); + } + */ + + public function onSourceSet(SourceSetEvent $event) { + $this->add_source_history($event->image, $event->source); + } + + public function onUserBlockBuilding(UserBlockBuildingEvent $event) { + global $user; + if($user->can("bulk_edit_image_tag")) { + $event->add_link("Source Changes", make_link("source_history/all/1")); + } + } + + protected function install() { + global $database, $config; + + if($config->get_int("ext_source_history_version") < 1) { + $database->create_table("source_histories", " + id SCORE_AIPK, + image_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + user_ip SCORE_INET NOT NULL, + source TEXT NOT NULL, + date_set DATETIME NOT NULL, + INDEX(image_id), + FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + "); + $config->set_int("ext_source_history_version", 3); + } + + if($config->get_int("ext_source_history_version") == 1) { + $database->Execute("ALTER TABLE source_histories ADD COLUMN user_id INTEGER NOT NULL"); + $database->Execute("ALTER TABLE source_histories ADD COLUMN date_set DATETIME NOT NULL"); + $config->set_int("ext_source_history_version", 2); + } + + if($config->get_int("ext_source_history_version") == 2) { + $database->Execute("ALTER TABLE source_histories ADD COLUMN user_ip CHAR(15) NOT NULL"); + $config->set_int("ext_source_history_version", 3); + } + } + + /* + * this function is called when a revert request is received + */ + private function process_revert_request($revert_id) { + global $page; + + $revert_id = int_escape($revert_id); + + // check for the nothing case + if($revert_id < 1) { + $page->set_mode("redirect"); + $page->set_redirect(make_link()); + return; + } + + // lets get this revert id assuming it exists + $result = $this->get_source_history_from_revert($revert_id); + + if(empty($result)) { + // there is no history entry with that id so either the image was deleted + // while the user was viewing the history, someone is playing with form + // variables or we have messed up in code somewhere. + /* calling die() is probably not a good idea, we should throw an Exception */ + die("Error: No source history with specified id was found."); + } + + // lets get the values out of the result + $stored_result_id = $result['id']; + $stored_image_id = $result['image_id']; + $stored_source = $result['source']; + + log_debug("source_history", 'Reverting source of Image #'.$stored_image_id.' to ['.$stored_source.']'); + // all should be ok so we can revert by firing the SetUserSources event. + send_event(new SourceSetEvent(Image::by_id($stored_image_id), $stored_source)); + + // all should be done now so redirect the user back to the image + $page->set_mode("redirect"); + $page->set_redirect(make_link('post/view/'.$stored_image_id)); + } + + protected function process_bulk_revert_request() { + if (isset($_POST['revert_name']) && !empty($_POST['revert_name'])) { + $revert_name = $_POST['revert_name']; + } + else { + $revert_name = null; + } + + if (isset($_POST['revert_ip']) && !empty($_POST['revert_ip'])) { + $revert_ip = filter_var($_POST['revert_ip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); + + if ($revert_ip === false) { + // invalid ip given. + $this->theme->display_admin_block('Invalid IP'); + return; + } + } + else { + $revert_ip = null; + } + + if (isset($_POST['revert_date']) && !empty($_POST['revert_date'])) { + if (isValidDate($_POST['revert_date']) ){ + $revert_date = addslashes($_POST['revert_date']); // addslashes is really unnecessary since we just checked if valid, but better safe. + } + else { + $this->theme->display_admin_block('Invalid Date'); + return; + } + } + else { + $revert_date = null; + } + + set_time_limit(0); // reverting changes can take a long time, disable php's timelimit if possible. + + // Call the revert function. + $this->process_revert_all_changes($revert_name, $revert_ip, $revert_date); + // output results + $this->theme->display_revert_ip_results(); + } + + public function get_source_history_from_revert(/*int*/ $revert_id) { + global $database; + $row = $database->get_row(" + SELECT source_histories.*, users.name + FROM source_histories + JOIN users ON source_histories.user_id = users.id + WHERE source_histories.id = ?", array($revert_id)); + return ($row ? $row : null); + } + + public function get_source_history_from_id(/*int*/ $image_id) { + global $database; + $row = $database->get_all(" + SELECT source_histories.*, users.name + FROM source_histories + JOIN users ON source_histories.user_id = users.id + WHERE image_id = ? + ORDER BY source_histories.id DESC", + array($image_id)); + return ($row ? $row : array()); + } + + public function get_global_source_history($page_id) { + global $database; + $row = $database->get_all(" + SELECT source_histories.*, users.name + FROM source_histories + JOIN users ON source_histories.user_id = users.id + ORDER BY source_histories.id DESC + LIMIT 100 OFFSET :offset + ", array("offset" => ($page_id-1)*100)); + return ($row ? $row : array()); + } + + /* + * This function attempts to revert all changes by a given IP within an (optional) timeframe. + */ + public function process_revert_all_changes($name, $ip, $date) { + global $database; + + $select_code = array(); + $select_args = array(); + + if(!is_null($name)) { + $duser = User::by_name($name); + if(is_null($duser)) { + $this->theme->add_status($name, "user not found"); + return; + } + else { + $select_code[] = 'user_id = ?'; + $select_args[] = $duser->id; + } + } + + if(!is_null($date)) { + $select_code[] = 'date_set >= ?'; + $select_args[] = $date; + } + + if(!is_null($ip)) { + $select_code[] = 'user_ip = ?'; + $select_args[] = $ip; + } + + if(count($select_code) == 0) { + log_error("source_history", "Tried to mass revert without any conditions"); + return; + } + + log_info("source_history", 'Attempting to revert edits where '.implode(" and ", $select_code)." (".implode(" / ", $select_args).")"); + + // Get all the images that the given IP has changed source on (within the timeframe) that were last editied by the given IP + $result = $database->get_col(' + SELECT t1.image_id + FROM source_histories t1 + LEFT JOIN source_histories t2 ON (t1.image_id = t2.image_id AND t1.date_set < t2.date_set) + WHERE t2.image_id IS NULL + AND t1.image_id IN ( select image_id from source_histories where '.implode(" AND ", $select_code).') + ORDER BY t1.image_id + ', $select_args); + + foreach($result as $image_id) { + // Get the first source history that was done before the given IP edit + $row = $database->get_row(' + SELECT id, source + FROM source_histories + WHERE image_id='.$image_id.' + AND NOT ('.implode(" AND ", $select_code).') + ORDER BY date_set DESC LIMIT 1 + ', $select_args); + + if (empty($row)) { + // we can not revert this image based on the date restriction. + // Output a message perhaps? + } + else { + $revert_id = $row['id']; + $result = $this->get_source_history_from_revert($revert_id); + + if(empty($result)) { + // there is no history entry with that id so either the image was deleted + // while the user was viewing the history, or something messed up + /* calling die() is probably not a good idea, we should throw an Exception */ + die('Error: No source history with specified id ('.$revert_id.') was found in the database.'."\n\n". + 'Perhaps the image was deleted while processing this request.'); + } + + // lets get the values out of the result + $stored_result_id = $result['id']; + $stored_image_id = $result['image_id']; + $stored_source = $result['source']; + + log_debug("source_history", 'Reverting source of Image #'.$stored_image_id.' to ['.$stored_source.']'); + // all should be ok so we can revert by firing the SetSources event. + send_event(new SourceSetEvent(Image::by_id($stored_image_id), $stored_source)); + $this->theme->add_status('Reverted Change','Reverted Image #'.$image_id.' to Source History #'.$stored_result_id.' ('.$row['source'].')'); + } + } + + log_info("source_history", 'Reverted '.count($result).' edits.'); + } + + /* + * this function is called just before an images source is changed + */ + private function add_source_history($image, $source) { + global $database, $config, $user; + + $new_source = $source; + $old_source = $image->source; + + if($new_source == $old_source) return; + + if(empty($old_source)) { + /* no old source, so we are probably adding the image for the first time */ + log_debug("source_history", "adding new source history: [$new_source]"); + } + else { + log_debug("source_history", "adding source history: [$old_source] -> [$new_source]"); + } + + $allowed = $config->get_int("history_limit"); + if($allowed == 0) return; + + // if the image has no history, make one with the old source + $entries = $database->get_one("SELECT COUNT(*) FROM source_histories WHERE image_id = ?", array($image->id)); + if($entries == 0 && !empty($old_source)) { + $database->execute(" + INSERT INTO source_histories(image_id, source, user_id, user_ip, date_set) + VALUES (?, ?, ?, ?, now())", + array($image->id, $old_source, $config->get_int('anon_id'), '127.0.0.1')); + $entries++; + } + + // add a history entry + $row = $database->execute(" + INSERT INTO source_histories(image_id, source, user_id, user_ip, date_set) + VALUES (?, ?, ?, ?, now())", + array($image->id, $new_source, $user->id, $_SERVER['REMOTE_ADDR'])); + $entries++; + + // if needed remove oldest one + if($allowed == -1) return; + if($entries > $allowed) { + // TODO: Make these queries better + /* + MySQL does NOT allow you to modify the same table which you use in the SELECT part. + Which means that these will probably have to stay as TWO separate queries... + + http://dev.mysql.com/doc/refman/5.1/en/subquery-restrictions.html + http://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause + */ + $min_id = $database->get_one("SELECT MIN(id) FROM source_histories WHERE image_id = ?", array($image->id)); + $database->execute("DELETE FROM source_histories WHERE id = ?", array($min_id)); + } + } +} +?> diff --git a/ext/source_history/theme.php b/ext/source_history/theme.php new file mode 100644 index 00000000..29bcc019 --- /dev/null +++ b/ext/source_history/theme.php @@ -0,0 +1,137 @@ + + ".make_form(make_link("source_history/revert"))." + + + + + "; + $history_html = $start_string . $history_list . $end_string; + + $page->set_title('Image '.$image_id.' Source History'); + $page->set_heading('Source History: '.$image_id); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Source History", $history_html, "main", 10)); + } + + public function display_global_page(Page $page, /*array*/ $history, /*int*/ $page_number) { + $start_string = " +
+ ".make_form(make_link("source_history/revert"))." + + + +
+ "; + + global $user; + $history_list = ""; + foreach($history as $fields) + { + $current_id = $fields['id']; + $image_id = $fields['image_id']; + $current_source = html_escape($fields['source']); + $name = $fields['name']; + $h_ip = $user->can("view_ip") ? " ".show_ip($fields['user_ip'], "Sourcing Image #$image_id as '$current_source'") : ""; + $setter = "".html_escape($name)."$h_ip"; + + $history_list .= ' +
  • + + '.$image_id.': + '.$current_source.' (Set by '.$setter.') +
  • + '; + } + + $history_html = $start_string . $history_list . $end_string; + $page->set_title("Global Source History"); + $page->set_heading("Global Source History"); + $page->add_block(new Block("Source History", $history_html, "main", 10)); + + + $h_prev = ($page_number <= 1) ? "Prev" : + 'Prev'; + $h_index = "Index"; + $h_next = 'Next'; + + $nav = $h_prev.' | '.$h_index.' | '.$h_next; + $page->add_block(new Block("Navigation", $nav, "left")); + } + + /* + * Add a section to the admin page. + */ + public function display_admin_block(/*string*/ $validation_msg='') { + global $page; + + if (!empty($validation_msg)) { + $validation_msg = '
    '. $validation_msg .''; + } + + $html = ' + Revert source changes/edit by a specific IP address or username. +
    You can restrict the time frame to revert these edits as well. +
    (Date format: 2011-10-23) + '.$validation_msg.' + +

    '.make_form(make_link("source_history/bulk_revert"), 'POST')." + + + + + +
    Username
    IP Address
    Date range
    + + "; + $page->add_block(new Block("Mass Source Revert", $html)); + } + + /* + * Show a standard page for results to be put into + */ + public function display_revert_ip_results() { + global $page; + $html = implode($this->messages, "\n"); + $page->add_block(new Block("Bulk Revert Results", $html)); + } + + public function add_status(/*string*/ $title, /*string*/ $body) { + $this->messages[] = '

    '. $title .'
    '. $body .'

    '; + } +} +?>