├── README.md ├── TrashmanWatchdog.module └── ProcessTrashman.module /README.md: -------------------------------------------------------------------------------- 1 | # Trashman module 2 | 3 | ProcessWire module which adds new admin page where users without superuser 4 | role can view and restore pages in trash. 5 | 6 | ## How to use this 7 | 8 | Install ProcessTrashman module, which automatically installs the required 9 | watchdog module. Installation creates new permission called "trashman". 10 | Give that new permission for all the roles that you want to have access to 11 | trash. After that users with those roles can view trash from Setup => Trashman. 12 | 13 | You can move or rename the trashman page if you want. Also visit the module 14 | settings to set date formats for your liking. 15 | 16 | ## Please note 17 | 18 | Trashman doesn't allow viewing or restoring pages that were trashed before 19 | Trashman was installed. This is by design, since I want to have trashed date. 20 | 21 | ## Licence 22 | 23 | GPL, just like [ProcessWire](https://github.com/ryancramerdesign/ProcessWire/blob/master/LICENSE.txt) 24 | 25 | ------ 26 | Trashman Copyright 2013 by Antti Peisa 27 | [ProcessWire](http://processwire.com) Copyright 2013 by Ryan Cramer 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /TrashmanWatchdog.module: -------------------------------------------------------------------------------- 1 | 'Trashman Watchdog', 28 | 'version' => 100, 29 | 'summary' => 'Autoload module, that saves the parent_before_trash value before page is trashed', 30 | 'href' => 'http://modules.processwire.com', 31 | 'singular' => true, 32 | 'autoload' => true, 33 | 'requires' => 'ProcessTrashman' 34 | ); 35 | } 36 | 37 | public function init() { 38 | $this->pages->addHookBefore('trash', $this, 'pageTrashed'); 39 | $this->pages->addHookAfter('restore', $this, 'removePageFromTrash'); 40 | $this->pages->addHookAfter('delete', $this, 'removePageFromTrash'); 41 | } 42 | 43 | /** 44 | * When page is trashed, we save a row for it on ProcessTrashman table 45 | * 46 | */ 47 | public function pageTrashed($event) { 48 | $page = $event->arguments[0]; 49 | if ($page->parent->id != $this->config->trashPageID) { 50 | $sql = "INSERT INTO ProcessTrashman SET page_id = {$page->id}, parent_id = {$page->parent->id} ON DUPLICATE KEY UPDATE parent_id = {$page->parent->id};"; 51 | $this->db->query($sql); 52 | } 53 | } 54 | 55 | public function removePageFromTrash($event) { 56 | $page = $event->arguments[0]; 57 | $sql = "DELETE FROM ProcessTrashman WHERE page_id = {$page->id};"; 58 | $this->db->query($sql); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /ProcessTrashman.module: -------------------------------------------------------------------------------- 1 | 'Trashman', 29 | 'summary' => 'Custom trash view for clients also.', 30 | 'version' => 100, 31 | 'author' => 'Antti Peisa', 32 | 'href' => 'http://modules.processwire.com/', 33 | 'permission' => 'trashman', 34 | 'installs' => 'TrashmanWatchdog' 35 | ); 36 | } 37 | 38 | private $dateFormat; 39 | 40 | const pageName = 'trashman'; 41 | 42 | static public function getDefaultData() { 43 | return array( 44 | 'dateInputFormat' => 'Y-m-d', 45 | 'timeInputFormat' => 'H:i:s', 46 | ); 47 | } 48 | 49 | public function init() { 50 | // we use date and time format combined when 51 | $this->dateFormat = $this->dateInputFormat . " " . $this->timeInputFormat; 52 | } 53 | 54 | public function __construct() { 55 | foreach (self::getDefaultData() as $key => $value) { 56 | $this->$key = $value; 57 | } 58 | } 59 | 60 | public function ___execute() { 61 | 62 | $out = ''; 63 | 64 | // Grab end value 65 | $end = ($this->input->get->end) ? strtotime($this->input->get->end) : strtotime("tomorrow -1second"); 66 | 67 | // If timeInputFormat is empty, then we need to add 23:59 to end value 68 | if ($this->timeInputFormat == "" && $this->input->get->end) 69 | $end = $end + 60 * 60 * 24 - 1; 70 | 71 | $start = ($this->input->get->start) ? strtotime($this->input->get->start) : $end - 60 * 60 * 24 * 32 + 1; 72 | 73 | // Mysql date formats for those 74 | $startDB = date('Y-m-d H:i:s', $start); 75 | $endDB = date('Y-m-d H:i:s', $end); 76 | 77 | // And nice readable formats 78 | $startNice = date($this->dateFormat, $start); 79 | $endNice = date($this->dateFormat, $end); 80 | 81 | if ($this->input->get->start) { 82 | $out .= "

" . sprintf($this->_('Showing pages that were removed between %1$s and %2$s'), $startNice, $endNice) . "

"; 83 | } else { 84 | $out .= "

" . $this->_('Showing pages that were removed in last 30 days') . "

"; 85 | } 86 | 87 | $form = $this->modules->get("InputfieldForm"); 88 | $form->method = "get"; 89 | 90 | $wrapper = $this->modules->get("InputfieldFieldset"); 91 | $wrapper->label = $this->_("Filters"); 92 | $wrapper->collapsed = Inputfield::collapsedYes; 93 | 94 | $startDate = $this->modules->get("InputfieldDatetime"); 95 | $startDate->name = "start"; 96 | $startDate->label = $this->_("Startdate"); 97 | $startDate->value = $start; 98 | $startDate->columnWidth = 50; 99 | $startDate->dateInputFormat = $this->dateInputFormat; 100 | $startDate->timeInputFormat = $this->timeInputFormat; 101 | 102 | $endDate = $this->modules->get("InputfieldDatetime"); 103 | $endDate->name = "end"; 104 | $endDate->label = $this->_("Enddate"); 105 | $endDate->value = $end; 106 | $endDate->columnWidth = 50; 107 | $endDate->dateInputFormat = $this->dateInputFormat; 108 | $endDate->timeInputFormat = $this->timeInputFormat; 109 | 110 | $submit = $this->modules->get("InputfieldSubmit"); 111 | 112 | // Build the form 113 | $wrapper->add($startDate); 114 | $wrapper->add($endDate); 115 | $wrapper->add($submit); 116 | $form->add($wrapper); 117 | 118 | // Find pages in trash that were removed between wanted dates 119 | $sql = "SELECT page_id, parent_id, trashed FROM {$this->className} WHERE trashed >= '$startDB' AND trashed <= '$endDB';"; 120 | $result = $this->db->query($sql); 121 | 122 | $pagesInTrash = new PageArray(); 123 | while ($row = $result->fetch_assoc()) { 124 | $p = $this->pages->get($row['page_id']); 125 | 126 | // If there is no page anymore, let's continue TODO: remove this from db also 127 | if (!$p->id) 128 | continue; 129 | 130 | // Add parent page and trashed date 131 | $p->oldParent = $this->pages->get($row['parent_id']); 132 | $p->trashed = $row['trashed']; 133 | $pagesInTrash->add($p); 134 | } 135 | 136 | // Render the form 137 | $out .= $form->render(); 138 | 139 | // Render results 140 | if ($pagesInTrash->count() == 0) { 141 | $out .= $this->_("Pages not found"); 142 | } else { 143 | 144 | $table = $this->modules->get("MarkupAdminDataTable"); 145 | $table->setEncodeEntities(false); 146 | $table->headerRow(array( 147 | $this->_("Page title"), 148 | $this->_("Number of children"), 149 | $this->_("Old parent"), 150 | $this->_("Date removed"), 151 | $this->_("Restore") 152 | )); 153 | 154 | foreach ($pagesInTrash as $p) { 155 | 156 | if (!$p->oldParent->id || $p->oldParent->isTrash()) { 157 | $parentPath = $this->_("unknown"); 158 | #$p->oldParent->id = 0; // causes page_id = 0 bug 159 | $parent = 0; 160 | } 161 | else { 162 | $parentPath = $p->oldParent->path; 163 | $parent = $p->oldParent->id; 164 | } 165 | 166 | 167 | $table->row(array( 168 | $p->get("title|name"), 169 | $p->children("include=all")->count(), 170 | $parentPath, 171 | $p->trashed, 172 | "" . $this->_("restore") . "" 173 | )); 174 | } 175 | 176 | $out .= $table->render(); 177 | } 178 | 179 | return $out; 180 | } 181 | 182 | public function ___executeRestore() { 183 | 184 | if ($this->input->post->restore_page) 185 | return $this->processInput(); 186 | 187 | $p = $this->pages->get($this->input->get->page_id); 188 | $out = ''; 189 | 190 | if (!$p->id) { 191 | $this->error($this->_("Page not found")); 192 | $this->session->redirect("../"); 193 | } 194 | 195 | if (!$p->isTrash()) { 196 | $this->error($this->_("Page is not in trash")); 197 | $this->session->redirect("../"); 198 | } 199 | 200 | // set a new headline, replacing the one used by our page (optional) 201 | Wire::setFuel('processHeadline', $this->_("Restore page")); 202 | 203 | $out .= "

$p->title

"; 204 | 205 | $form = $this->modules->get("InputfieldForm"); 206 | $form->action = "./"; 207 | $form->method = "post"; 208 | 209 | $field = $this->modules->get("InputfieldPageListSelect"); 210 | $field->parent_id = 0; 211 | $field->value = (int) $this->input->get->parent_id; 212 | $field->required = true; 213 | $field->label = $this->_("Parent page for the restored page"); 214 | $field->description = sprintf($this->_("'%s' will be restored under selected page. Please note that the restored page will be unpublished, so you can edit it before publishing."), $p->get("title|name")); 215 | $field->name = "parent_id"; 216 | $form->add($field); 217 | 218 | $field = $this->modules->get("InputfieldHidden"); 219 | $field->name = "page_id"; 220 | $field->value = $p->id; 221 | $form->add($field); 222 | 223 | $submit = $this->modules->get("InputfieldSubmit"); 224 | $submit->name = "restore_page"; 225 | $form->add($submit); 226 | 227 | $out .= $form->render(); 228 | $out .= "

" . $this->_("Go Back") . "

"; 229 | 230 | // add a breadcrumb that returns to our main page 231 | $this->breadcrumbs->add(new Breadcrumb('../', $this->page->title)); 232 | 233 | return $out; 234 | } 235 | 236 | public function processInput() { 237 | if (!$this->input->post->parent_id || !$this->input->post->page_id) { 238 | $this->error($this->_("Error while trying to restore the page")); 239 | return "" . $this->_("Go back") . ""; 240 | } 241 | 242 | $page = $this->pages->get($this->input->post->page_id); 243 | $page->parent = $this->pages->get($this->input->post->parent_id); 244 | $page->addStatus(Page::statusUnpublished); 245 | 246 | $this->pages->restore($page); 247 | $this->message($this->_("Page restored, but kept unpublished")); 248 | 249 | $admin = $this->pages->get($this->config->adminRootPageID); 250 | $this->session->redirect($admin->url . "page/edit/?id=$page->id"); 251 | } 252 | 253 | /** 254 | * Module configuration 255 | * 256 | * @param array $data 257 | * @return InputfieldWrapper 258 | */ 259 | static public function getModuleConfigInputfields(array $data) { 260 | 261 | // this is a container for fields, basically like a fieldset 262 | $fields = new InputfieldWrapper(); 263 | 264 | // since this is a static function, we can't use $this->modules, so get them from the global wire() function 265 | $modules = wire('modules'); 266 | 267 | // merge default config settings (custom values overwrite defaults) 268 | $defaults = self::getDefaultData(); 269 | $data = array_merge($defaults, $data); 270 | 271 | // date format used 272 | $field = $modules->get("InputfieldText"); 273 | $field->name = "dateInputFormat"; 274 | $field->label = "Date Format"; 275 | $field->notes = "See the [PHP date](http://www.php.net/manual/en/function.date.php) function reference for more information on how to customize this format."; 276 | $field->value = $data['dateInputFormat']; 277 | $field->size = 70; 278 | $fields->add($field); 279 | 280 | // time format used 281 | $field = $modules->get("InputfieldText"); 282 | $field->name = "timeInputFormat"; 283 | $field->label = "Time Format"; 284 | $field->description = "Leave this empty, if you don't need to filter by time"; 285 | $field->notes = "See the [PHP date](http://www.php.net/manual/en/function.date.php) function reference for more information on how to customize this format."; 286 | $field->value = $data['timeInputFormat']; 287 | $field->size = 70; 288 | $fields->add($field); 289 | 290 | return $fields; 291 | } 292 | 293 | /** 294 | * Called only when your module is installed 295 | * 296 | * This version creates a new page with this Process module assigned. 297 | * 298 | */ 299 | public function ___install() { 300 | 301 | $permission = $this->permissions->add("trashman"); 302 | $permission->title = "Can view and restore pages in trash"; 303 | $permission->save(); 304 | $this->session->message("Created new permission: trashman"); 305 | 306 | $page = new Page(); 307 | $page->template = 'admin'; 308 | $page->name = self::pageName; 309 | $page->parent = $this->pages->get($this->config->adminRootPageID)->child('name=setup'); 310 | $page->process = $this; 311 | $page->title = "Trashman"; 312 | $page->save(); 313 | 314 | $this->message("Created Page: {$page->path}"); 315 | 316 | $sql = <<< _END 317 | 318 | CREATE TABLE {$this->className} ( 319 | id int unsigned NOT NULL auto_increment, 320 | page_id int unsigned DEFAULT 0, 321 | parent_id int unsigned DEFAULT 0, 322 | trashed TIMESTAMP, 323 | PRIMARY KEY(id), 324 | UNIQUE KEY(page_id) 325 | ) ENGINE = MYISAM; 326 | 327 | _END; 328 | 329 | $this->db->query($sql); 330 | 331 | $this->message("Created database table: {$this->className}"); 332 | } 333 | 334 | /** 335 | * Called only when your module is uninstalled 336 | * 337 | * This should return the site to the same state it was in before the module was installed. 338 | * 339 | */ 340 | public function ___uninstall() { 341 | 342 | $permission = $this->permissions->get("trashman"); 343 | $this->permissions->delete($permission); 344 | 345 | // find the page we installed, locating it by the process field (which has the module ID) 346 | // it would probably be sufficient just to locate by name, but this is just to be extra sure. 347 | $moduleID = $this->modules->getModuleID($this); 348 | $page = $this->pages->get("template=admin, process=$moduleID, name=" . self::pageName); 349 | 350 | if ($page->id) { 351 | // if we found the page, let the user know and delete it 352 | $this->message("Deleting Page: {$page->path}"); 353 | $page->delete(); 354 | } 355 | 356 | $this->db->query("DROP TABLE {$this->className}"); 357 | } 358 | 359 | } 360 | 361 | --------------------------------------------------------------------------------