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