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