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?>