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