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 echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '">'; 90 echo $value->getValueEditor('entry'); 91 echo '</label>'; 92 $hint = $this->column->getType()->getTranslatedHint(); 93 if($hint) { 94 echo '<div class="hint">'; 95 echo hsc($hint); 96 echo '</div>'; 97 } 98 99 // csrf protection 100 formSecurityToken(); 101 } 102 103 /** 104 * Save the data posted by the inline editor 105 */ 106 protected function inline_save() { 107 global $INPUT; 108 109 // check preconditions 110 if(!$this->initFromInput()) { 111 throw new StructException('inline save error: init'); 112 } 113 self::checkCSRF(); 114 if(!$this->schemadata->getSchema()->isLookup()) { 115 $this->checkPage(); 116 $assignments = Assignments::getInstance(); 117 $tables = $assignments->getPageAssignments($this->pid, true); 118 if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) { 119 throw new StructException('inline save error: schema not assigned to page'); 120 } 121 } 122 if(!$this->schemadata->getSchema()->isEditable()) { 123 throw new StructException('inline save error: no permission for schema'); 124 } 125 126 // validate 127 $value = $INPUT->param('entry'); 128 $validator = new ValueValidator(); 129 if(!$validator->validateValue($this->column, $value)) { 130 throw new StructException(join("\n", $validator->getErrors())); 131 } 132 133 // current data 134 $tosave = $this->schemadata->getDataArray(); 135 $tosave[$this->column->getLabel()] = $value; 136 137 // save 138 if($this->schemadata->getSchema()->isLookup()) { 139 $revision = 0; 140 } else { 141 $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit'); 142 p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table 143 } 144 $this->schemadata->setTimestamp($revision); 145 try { 146 if(!$this->schemadata->saveData($tosave)) { 147 throw new StructException('saving failed'); 148 } 149 if(!$this->schemadata->getSchema()->isLookup()) { 150 // make sure this schema is assigned 151 $assignments->assignPageSchema( 152 $this->pid, 153 $this->schemadata->getSchema()->getTable() 154 ); 155 } 156 } finally { 157 // unlock (unlocking a non-existing file is okay, 158 // so we don't check if it's a lookup here 159 unlock($this->pid); 160 } 161 162 // reinit then render 163 $this->initFromInput(); 164 $value = $this->schemadata->getDataColumn($this->column); 165 $R = new Doku_Renderer_xhtml(); 166 $value->render($R, 'xhtml'); // FIXME use configured default renderer 167 echo $R->doc; 168 } 169 170 /** 171 * Unlock a page (on cancel action) 172 */ 173 protected function inline_cancel() { 174 global $INPUT; 175 $pid = $INPUT->str('pid'); 176 unlock($pid); 177 } 178 179 /** 180 * Initialize internal state based on input variables 181 * 182 * @return bool if initialization was successfull 183 */ 184 protected function initFromInput() { 185 global $INPUT; 186 187 $this->schemadata = null; 188 $this->column = null; 189 190 $pid = $INPUT->str('pid'); 191 list($table, $field) = explode('.', $INPUT->str('field')); 192 if(blank($pid)) return false; 193 if(blank($table)) return false; 194 if(blank($field)) return false; 195 196 $this->pid = $pid; 197 try { 198 $this->schemadata = AccessTable::byTableName($table, $pid); 199 } catch(StructException $ignore) { 200 return false; 201 } 202 203 $this->column = $this->schemadata->getSchema()->findColumn($field); 204 if(!$this->column || !$this->column->isVisibleInEditor()) { 205 $this->schemadata = null; 206 $this->column = null; 207 return false; 208 } 209 210 return true; 211 } 212 213 /** 214 * Checks if a page can be edited 215 * 216 * @throws StructException when check fails 217 */ 218 protected function checkPage() { 219 if(!page_exists($this->pid)) { 220 throw new StructException('inline save error: no such page'); 221 } 222 if(auth_quickaclcheck($this->pid) < AUTH_EDIT) { 223 throw new StructException('inline save error: acl'); 224 } 225 if(checklock($this->pid)) { 226 throw new StructException('inline save error: lock'); 227 } 228 } 229 230 /** 231 * Our own implementation of checkSecurityToken because we don't want the msg() call 232 * 233 * @throws StructException when check fails 234 */ 235 public static function checkCSRF() { 236 global $INPUT; 237 if( 238 $INPUT->server->str('REMOTE_USER') && 239 getSecurityToken() != $INPUT->str('sectok') 240 ) { 241 throw new StructException('CSRF check failed'); 242 } 243 } 244 245} 246