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