1<?php 2/** 3 * DokuWiki Plugin struct (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr, Michael Große <dokuwiki@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10use dokuwiki\plugin\struct\meta\AccessTable; 11use dokuwiki\plugin\struct\meta\AccessTableData; 12use dokuwiki\plugin\struct\meta\Column; 13use dokuwiki\plugin\struct\meta\StructException; 14use dokuwiki\plugin\struct\meta\ValueValidator; 15 16if(!defined('DOKU_INC')) die(); 17 18/** 19 * Class action_plugin_struct_inline 20 * 21 * Handle inline editing 22 */ 23class action_plugin_struct_inline extends DokuWiki_Action_Plugin { 24 25 /** @var AccessTableData */ 26 protected $schemadata = null; 27 28 /** @var Column */ 29 protected $column = null; 30 31 /** @var String */ 32 protected $pid = ''; 33 34 /** 35 * Registers a callback function for a given event 36 * 37 * @param Doku_Event_Handler $controller DokuWiki's event controller object 38 * @return void 39 */ 40 public function register(Doku_Event_Handler $controller) { 41 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax'); 42 } 43 44 /** 45 * @param Doku_Event $event 46 * @param $param 47 */ 48 public function handle_ajax(Doku_Event $event, $param) { 49 $len = strlen('plugin_struct_inline_'); 50 if(substr($event->data, 0, $len) != 'plugin_struct_inline_') return; 51 $event->preventDefault(); 52 $event->stopPropagation(); 53 54 if(substr($event->data, $len) == 'editor') { 55 $this->inline_editor(); 56 } 57 58 if(substr($event->data, $len) == 'save') { 59 try { 60 $this->inline_save(); 61 } catch(StructException $e) { 62 http_status(500); 63 header('Content-Type: text/plain; charset=utf-8'); 64 echo $e->getMessage(); 65 } 66 } 67 68 if(substr($event->data, $len) == 'cancel') { 69 $this->inline_cancel(); 70 } 71 } 72 73 /** 74 * Creates the inline editor 75 */ 76 protected function inline_editor() { 77 // silently fail when editing not possible 78 if(!$this->initFromInput()) return; 79 if(auth_quickaclcheck($this->pid) < AUTH_EDIT) return; 80 if(!$this->schemadata->getSchema()->isEditable()) return; 81 if(checklock($this->pid)) return; 82 83 // lock page 84 lock($this->pid); 85 86 // output the editor 87 $value = $this->schemadata->getDataColumn($this->column); 88 echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '">'; 89 echo $value->getValueEditor('entry'); 90 echo '</label>'; 91 $hint = $this->column->getType()->getTranslatedHint(); 92 if($hint) { 93 echo '<div class="hint">'; 94 echo hsc($hint); 95 echo '</div>'; 96 } 97 98 // csrf protection 99 formSecurityToken(); 100 } 101 102 /** 103 * Save the data posted by the inline editor 104 */ 105 protected function inline_save() { 106 global $INPUT; 107 108 // check preconditions 109 if(!$this->initFromInput()) { 110 throw new StructException('inline save error: init'); 111 } 112 self::checkCSRF(); 113 if(!$this->schemadata->getSchema()->isLookup()) { 114 $this->checkPage(); 115 } 116 if(!$this->schemadata->getSchema()->isEditable()) { 117 throw new StructException('inline save error: no permission for schema'); 118 } 119 120 // validate 121 $value = $INPUT->param('entry'); 122 $validator = new ValueValidator(); 123 if(!$validator->validateValue($this->column, $value)) { 124 throw new StructException(join("\n", $validator->getErrors())); 125 } 126 127 // current data 128 $tosave = $this->schemadata->getDataArray(); 129 $tosave[$this->column->getLabel()] = $value; 130 131 // save 132 if($this->schemadata->getSchema()->isLookup()) { 133 $revision = 0; 134 } else { 135 $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit'); 136 p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table 137 } 138 $this->schemadata->setTimestamp($revision); 139 try { 140 if(!$this->schemadata->saveData($tosave)) { 141 throw new StructException('saving failed'); 142 } 143 } finally { 144 // unlock (unlocking a non-existing file is okay, 145 // so we don't check if it's a lookup here 146 unlock($this->pid); 147 } 148 149 // reinit then render 150 $this->initFromInput(); 151 $value = $this->schemadata->getDataColumn($this->column); 152 $R = new Doku_Renderer_xhtml(); 153 $value->render($R, 'xhtml'); // FIXME use configured default renderer 154 echo $R->doc; 155 } 156 157 /** 158 * Unlock a page (on cancel action) 159 */ 160 protected function inline_cancel() { 161 global $INPUT; 162 $pid = $INPUT->str('pid'); 163 unlock($pid); 164 } 165 166 /** 167 * Initialize internal state based on input variables 168 * 169 * @return bool if initialization was successfull 170 */ 171 protected function initFromInput() { 172 global $INPUT; 173 174 $this->schemadata = null; 175 $this->column = null; 176 177 $pid = $INPUT->str('pid'); 178 list($table, $field) = explode('.', $INPUT->str('field')); 179 if(blank($pid)) return false; 180 if(blank($table)) return false; 181 if(blank($field)) return false; 182 183 $this->pid = $pid; 184 try { 185 $this->schemadata = AccessTable::byTableName($table, $pid); 186 } catch(StructException $ignore) { 187 return false; 188 } 189 190 $this->column = $this->schemadata->getSchema()->findColumn($field); 191 if(!$this->column || !$this->column->isVisibleInEditor()) { 192 $this->schemadata = null; 193 $this->column = null; 194 return false; 195 } 196 197 return true; 198 } 199 200 /** 201 * Checks if a page can be edited 202 * 203 * @throws StructException when check fails 204 */ 205 protected function checkPage() { 206 if(!page_exists($this->pid)) { 207 throw new StructException('inline save error: no such page'); 208 } 209 if(auth_quickaclcheck($this->pid) < AUTH_EDIT) { 210 throw new StructException('inline save error: acl'); 211 } 212 if(checklock($this->pid)) { 213 throw new StructException('inline save error: lock'); 214 } 215 } 216 217 /** 218 * Our own implementation of checkSecurityToken because we don't want the msg() call 219 * 220 * @throws StructException when check fails 221 */ 222 public static function checkCSRF() { 223 global $INPUT; 224 if( 225 $INPUT->server->str('REMOTE_USER') && 226 getSecurityToken() != $INPUT->str('sectok') 227 ) { 228 throw new StructException('CSRF check failed'); 229 } 230 } 231 232} 233