1<?php 2/** 3 * Project: a class that implements the management of a project 4 * 5 * @author Junling Ma <junlingm@gmail.com> 6 */ 7 8require_once(dirname(__FILE__).'/../conf.php'); 9require_once(dirname(__FILE__).'/project_file.php'); 10require_once(dirname(__FILE__).'/maker.php'); 11require_once(dirname(__FILE__).'/mutex.php'); 12 13function unlock_with_error() { 14 $project = Project::project(); 15 if ($project) $project->unlock(); 16 17/* 18if(!is_null($e = error_get_last())) { 19 header('content-type: text/plain'); 20 print "Error occurred:\n\n". print_r($e,true); 21 } 22*/ 23} 24 25register_shutdown_function('unlock_with_error', E_ERROR); 26 27class Project extends DOMDocument { 28 private static $projects = array(); 29 30 public static function reload_project($project_ID = NULL) { 31 unset(self::$projects[$project_ID]); 32 return self::$projects($project_ID, false); 33 } 34 35 public static function project($project_ID = NULL, $create = false) { 36 if (!$project_ID) { 37 global $ID; 38 $project_ID = getNS($ID); 39 } 40 if (isset(self::$projects[$project_ID])) 41 return self::$projects[$project_ID]; 42 $name = noNS($project_ID); 43 $project_file = DOKU_DATA . implode('/', explode(':', $project_ID)) 44 . "/$name.project"; 45 if (file_exists($project_file)) { 46 $project = unserialize(file_get_contents($project_file)); 47 if (!method_exists($project, 'version') || 48 $project->version() != PROJECTS_VERSION) { 49 $project = new Project($project_ID); 50 $project->rebuild(); 51 } 52 } 53 else if ($create) 54 $project = new Project($project_ID); 55 else return NULL; 56 self::$projects[$project_ID] = $project; 57 return $project; 58 } 59 60 // the namespace ID of the project 61 private $ID = NULL; 62 // the path to the project dir 63 private $project_path = NULL; 64 // the path to the project file 65 private $project_file = NULL; 66 // the list of files defined in the project 67 private $files = array(); 68 // the list of errors 69 private $errors = array(); 70 // whether the content is modified 71 private $modified = false; 72 // mutex 73 private $mutex = NULL; 74 // version string 75 private $version_string = NULL; 76 77 /** 78 * The constructor, this creates a new project. 79 * Taking an array that specifies the path a project. 80 * This array is created from the wiki namespace path 81 */ 82 public function __construct($ID) { 83 $this->ID = $ID; 84 $this->project_path = DOKU_DATA . implode('/', explode(":", $ID)) . '/'; 85 $this->project_file = $this->project_path . 86 noNS($this->ID) . '.project'; 87 $this->mutex = new Mutex($this->project_file); 88 $this->version_string = PROJECTS_VERSION; 89 $this->create(); 90 } 91 92 /** 93 * The destructor. saves project is not saved 94 */ 95 function __destruct() { 96 $this->save_project(false); 97// $this->mutex->release(); 98 } 99 100 public function version() { 101 if (isset($this->version_string)) 102 return $this->version_string; 103 return NULL; 104 } 105 /** 106 * create a project 107 * 108 */ 109 protected function create() { 110 // make sure the parent exists 111 $parent = $this->parent(true); 112 // create the project dir 113 @mkdir($this->project_path, PROJECTS_PERMISSIONS, true); 114 // create an empty project file 115 $this->modified = true; 116 $this->save_project(); 117 } 118 119 public function path() { return $this->project_path; } 120 public function project_file() { return $this->project_file; } 121 public function name() { return $this->ID; } 122 123 // return the page id of a file with given name 124 public function id($name) { 125 if ($this->file($name) == NULL) return NULL; 126 return $this->ID . ":$name"; 127 } 128 129 public function parent($create = false) { 130 $parent = getNS($this->ID); 131 if (!$parent) return NULL; 132 return self::project(getNS($this->ID), $create); 133 } 134 135 /** 136 * save the project 137 * 138 */ 139 protected function save_project() { 140 if (!$this->modified) return; 141 $this->modified = false; 142 file_put_contents($this->project_file, serialize($this)); 143 } 144 145 /** 146 * delete this project 147 * 148 */ 149 public function delete() { 150 delete_dir($this->project_path); 151 } 152 153 public function files() { 154 return $this->files; 155 } 156 157 public function file($name) { 158 if (!isset($this->files[$name])) return NULL; 159 return $this->files[$name]; 160 } 161 162 public function errors() { 163 return $this->errors; 164 } 165 166 public function error($name) { 167 if (isset($this->errors[$name])) return $this->errors[$name]; 168 return NULL; 169 } 170 171 public function changed() { return $this->changed; } 172 173 // remove a file from the project 174 public function remove_file($name) { 175 if (!$this->remove_file_without_remake($name)) return false; 176 return $this->remake(); 177 } 178 179 private function remove_file_without_remake($name) { 180 if (!$this->mutex->acquire()) return false; 181 if (isset($this->files[$name])) { 182 $this->files[$name]->delete($this->project_path); 183 unset($this->files[$name]); 184 } 185 if (isset($this->errors[$name])) unset($this->errors[$name]); 186 // if this file is in the changed list, drop it from the list 187 $key = array_search($name, $this->changed); 188 if ($key !== false) unset($this->changed[$key]); 189 $this->modified = true; 190 $this->save_project(); 191 $this->mutex->release(); 192 return true; 193 } 194 195 public function update_file($file) { 196 $file = $this->handle($file); 197 if ($file == NULL) return true; 198 if (!$this->mutex->acquire()) return false; 199 // let the plugins handle the file, if needed 200 $name = $file->name(); 201 // check if it is a new file not registered in the project 202 if (!isset($this->files[$name])) 203 $this->files[$name] = ProjectFile::create($this, $file); 204 else { 205 $old = $this->files[$name]; 206 // check if two files are the same type, if not, delete the old 207 if ($old->type() != $file->type()) { 208 $this->mutex->release(); 209 if (!$this->remove_file_without_remake($name, false)) return false; 210 return $this->update_file($file); 211 } 212 // check if two files are the same 213 if ($old->equal($this->project_path, $file)) { 214 $this->mutex->release(); 215 return true; 216 } 217 // copy to the project 218 $old->copy($this->project_path, $file); 219 } 220 // this file has been changed, project needs to be remade 221 $this->modified = true; 222 $this->save_project(); 223 $this->mutex->release(); 224 $this->remake(array($name)); 225 return true; 226 } 227 228 // returns NULL if no default rule can make $name 229 // otherwise return a target file that can make it 230 public function handle($file) { 231 $plugins = new Plugins(PROJECTS_PLUGINS_FILE_DIR); 232 $handlers = $plugins->handlers($this, $file); 233 if (!$handlers) return $file; 234 reset($handlers); 235 $handler = current($handlers); 236 return $handler->handle($this, $file); 237 } 238 239 public function remake($files = array()) { 240 $files = array_merge($files, $this->files_need_update()); 241 $files = array_keys(array_flip($files)); 242 if (!$this->mutex->acquire()) return false; 243 $maker = new Maker($this); 244 foreach ($this->files as $name => $file) 245 if ($file->type() == CROSSLINK && !in_array($nme, $files)) 246 $files[] = $name; 247 if ($files) $files = $maker->update($files); 248 $this->errors = $maker->errors(); 249 // those that have failed to make will be deleted 250 foreach ($this->errors as $name => $error) { 251 $file = $this->files[$name]; 252 if ($file) { 253 if ($file->is_target()) $file->delete($this->path()); 254 } 255 else { 256 $path = $this->project_path . $name; 257 if (file_exists($path)) unlink($path); 258 } 259 } 260 foreach ($files as $name) 261 if (!isset($this->errors[$name])) { 262 if (!isset($this->files[$name])) continue; 263 $file = $this->files[$name]; 264 if ($file->is_target()) 265 $file->set_last_made_time($this->path()); 266 } 267 $this->modified = true; 268 // should not remake when saving. otherwise infinite loop 269 $this->save_project(); 270 $this->mutex->release(); 271 return true; 272 } 273 274 public function clean($recursive = true) { 275 if (!$this->mutex->acquire()) return false; 276 if (($dh = opendir($this->project_path)) === false) { 277 $this->mutex->release(); 278 return true; 279 } 280 while (($file = readdir($dh)) !== false) { 281 if ($file === '.' || $file === '..') continue; 282 $path = $this->project_path . $file; 283 if (is_dir($path)) { 284 $sub_project = self::project($this->name . ":" . $file); 285 if ($sub_project === NULL) delete_dir($path . '/'); 286 else if ($recursive) { 287 if ($sub_project->clean()) continue; 288 $this->mutex->release(); 289 return false; 290 } 291 } 292 // is not a project file and not a lock 293 $file = $this->file($file); 294 if ($file === NULL) { 295 if ($path != $this->project_file . '.project.lock') 296 @unlink($path); 297 } 298 } 299 closedir($dh); 300 foreach ($this->files as $file) 301 if ($file->is_target()) $this->changed[] = $file->name(); 302 $this->modified = true; 303 $this->save_project(); 304 $this->mutex->release(); 305 return true; 306 } 307 308 public function rebuild() { 309 if (!$this->mutex->acquire()) return false; 310 @unlink($this->project_file); 311 $this->files = array(); 312 $this->errors = array(); 313 $this->modified = true; 314 $this->save_project(false); 315 $this->mutex->release(); 316 $this->clean(); 317 318 global $ID; 319 $pages_path = DOKU_DATA . 'pages/' . implode('/', 320 explode(':', $this->ID)) . '/'; 321 if (($dh = opendir($pages_path)) === false) return true; 322 while (($file = readdir($dh)) !== false) { 323 if ($file === '.' || $file === '..' || 324 !has_extension($file, '.txt')) continue; 325 if (is_dir($path)) continue; 326 $file_id = substr($file, 0, -4); 327 $this->sync($file_id); 328 } 329 closedir($dh); 330 return true; 331 } 332 333 public function unlock() { 334 $this->mutex->release(); 335 } 336 337 private function sync($id) { 338 global $ID; 339 // save $ID 340 $save_ID = $ID; 341 $pages_path = DOKU_DATA . 'pages/' . implode('/', 342 explode(':', $this->ID)) . '/'; 343 $path = $pages_path . $id . '.txt'; 344 $ID = $this->ID . ":" . $id; 345 // clear cache 346 $cache = new cache_renderer($ID, $path, 'metadata'); 347 $cache->removeCache(); 348 $cache = new cache_renderer($ID, $path, 'xhtml'); 349 $cache->removeCache(); 350 $cache = new cache_instructions($ID, $path); 351 $cache->removeCache(); 352 p_cached_output($path, 'metadata', $ID); 353 // restore $ID 354 $ID = $save_ID; 355 } 356 357 private function files_need_update() { 358 $changed = array(); 359 foreach ($this->files as $name => $file) 360 if ($file->is_target() && $file->needs_update($this->path())) 361 $changed[] = $name; 362 return $changed; 363 } 364 365 public function subprojects() { 366 $subprojects = array(); 367 if (($dh = opendir($this->project_path)) === false) return array(); 368 while (($file = readdir($dh)) !== false) { 369 if ($file === '.' || $file === '..') continue; 370 $path = $this->project_path . $file; 371 if (!is_dir($path) || self::project($this->ID . ":$file", false) == NULL) 372 continue; 373 $subprojects[] = $file; 374 } 375 closedir($dh); 376 return $subprojects; 377 } 378 379} 380 381?>