00001 <?php
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012 @require_once('config.inc.php');
00013 require_once('common.inc.php');
00014
00015 require_once('modules.inc.php');
00016 require_once('util.inc.php');
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033 function _cmp_time($a, $b)
00034 {
00035 if ($a['time'] == $b['time']) {
00036 return 0;
00037 }
00038 return ($a['time'] < $b['time']) ? 1 : -1;
00039 }
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051 function _obj_lock($name, $wait = true)
00052 {
00053
00054
00055
00056 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
00057 log_msg('warn', 'lock: locking is not supported on WIN32 at the moment');
00058 return true;
00059 }
00060
00061 $start = intval(microtime(true)*1000.0);
00062 $fn = CONTENT_DIR.'/'.str_replace('.', '/', $name);
00063
00064 if (@is_link($fn)) {
00065 $target = @readlink($fn);
00066 if (substr($target, 0, 1) == '/') {
00067 $fn = $target;
00068 } else {
00069 $fn = dirname($fn).'/'.$target;
00070 }
00071 log_msg('debug', 'lock: resolved '.$name.' -> '.$fn);
00072 }
00073 do {
00074 $f = @fopen($fn, 'rb');
00075 if ($f === false) {
00076
00077 log_msg('debug', 'lock: file '.$fn.' does not exist');
00078 return NULL;
00079 }
00080
00081 if (@flock($f, LOCK_EX|LOCK_NB)) {
00082
00083 log_msg('debug', 'lock: acquired lock for '.$name);
00084 return $f;
00085 } elseif ($wait === false) {
00086
00087 log_msg('debug', 'lock: could not acquire lock');
00088 return false;
00089 } elseif (is_int($wait) && $wait < abs(intval(microtime(true)*1000.0)-$start)) {
00090
00091 log_msg('debug', 'lock: could not acquire lock in '.$wait.'ms');
00092 return false;
00093 }
00094
00095 usleep(100000);
00096 } while (true);
00097 }
00098
00099
00100
00101
00102
00103
00104
00105 function _obj_unlock($f)
00106 {
00107
00108 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
00109 log_msg('warn', 'unlock: locking is not supported on WIN32 at the moment');
00110 return;
00111 }
00112
00113 if ($f) {
00114 @flock($f, LOCK_UN);
00115 log_msg('debug', 'lock: released lock');
00116 @fclose($f);
00117 }
00118 }
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131 function check_auto_snapshot($args)
00132 {
00133 if (!isset($args['page'])) {
00134 return response('Required argument "page" missing', 400);
00135 }
00136 if (!page_exists($args['page'])) {
00137 return response('Page '.quot($args['page']).' does not exist', 400);
00138 }
00139
00140 $a = expl('.', $args['page']);
00141 $revs = revisions_info(array('pagename'=>$a[0], 'sort'=>'time'));
00142 $revs = $revs['#data'];
00143
00144 if ($a[1] == 'head' && SNAPSHOT_MIN_AGE != 0) {
00145
00146
00147 for ($i=0; $i < count($revs); $i++) {
00148 if (substr($revs[$i]['revision'], 0, 5) == 'auto-') {
00149
00150 if (time()-$revs[$i]['time'] < SNAPSHOT_MIN_AGE) {
00151 log_msg('debug', 'check_auto_snapshot: age is '.(time()-$revs[$i]['time']).' seconds, not creating a snapshot');
00152 break;
00153 }
00154
00155 if (dir_is_different(CONTENT_DIR.'/'.str_replace('.', '/', $args['page']), CONTENT_DIR.'/'.str_replace('.', '/', $revs[$i]['page']))) {
00156 snapshot($args);
00157 } else {
00158 log_msg('debug', 'check_auto_snapshot: head is identical to '.$revs[$i]['revision'].', not creating a snapshot');
00159 }
00160 break;
00161 }
00162 if ($i == count($revs)-1) {
00163
00164 snapshot($args);
00165 }
00166 }
00167 }
00168
00169
00170 if (SNAPSHOT_MAX_AGE != 0) {
00171 for ($i=count($revs)-1; 0 <= $i; $i--) {
00172 if (substr($revs[$i]['revision'], 0, 5) == 'auto-' && SNAPSHOT_MAX_AGE < time()-$revs[$i]['time']) {
00173 log_msg('info', 'check_auto_snapshot: deleting an old snapshot');
00174 delete_page(array('page'=>$revs[$i]['page']));
00175 $i--;
00176 }
00177 }
00178 }
00179
00180 return response(true);
00181 }
00182
00183 register_service('glue.check_auto_snapshot', 'check_auto_snapshot', array('auth'=>true));
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194 function clone_object($args)
00195 {
00196
00197 $old = load_object($args);
00198 if ($old['#error']) {
00199 return $old;
00200 } else {
00201 $old = $old['#data'];
00202 }
00203
00204
00205 $a = expl('.', $old['name']);
00206 $new = create_object(array('page'=>$a[0].'.'.$a[1]));
00207 if ($new['#error']) {
00208 return $new;
00209 } else {
00210 $new = $new['#data'];
00211 }
00212
00213
00214 $new = array_merge($old, $new);
00215 $ret = save_object($new);
00216 if ($ret['#error']) {
00217 return $ret;
00218 } else {
00219
00220 return response($new['name']);
00221 }
00222 }
00223
00224 register_service('glue.clone_object', 'clone_object', array('auth'=>true));
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235 function create_object($args)
00236 {
00237 if (!isset($args['page'])) {
00238 return response('Required argument "page" missing', 400);
00239 }
00240 if (!page_exists($args['page'])) {
00241 return response('Page '.quot($args['page']).' does not exist', 400);
00242 }
00243
00244
00245 $f = false;
00246 $tries = 0;
00247 $mtime = microtime(true);
00248 do {
00249
00250 $name = $args['page'].'.'.intval(floor($mtime)).intval(($mtime-floor($mtime))*100.0+$tries);
00251 $m = umask(0111);
00252 $f = @fopen(CONTENT_DIR.'/'.str_replace('.', '/', $name), 'x');
00253 umask($m);
00254 }
00255 while ($f === false && $tries++ < 9);
00256
00257 if (!$f) {
00258 return response('Error creating an object in page '.quot($args['page']), 500);
00259 } else {
00260 fclose($f);
00261 log_msg('info', 'create_object: created '.quot($name));
00262 return response(array('name'=>$name));
00263 }
00264 }
00265
00266 register_service('glue.create_object', 'create_object', array('auth'=>true));
00267
00268
00269
00270
00271
00272
00273
00274
00275
00276 function create_page($args)
00277 {
00278 if (empty($args['page'])) {
00279 return response('Required argument "page" missing or empty', 400);
00280 }
00281 if (page_exists($args['page'])) {
00282 return response('Page '.quot($args['page']).' already exists', 400);
00283 }
00284 if (!valid_pagename($args['page'])) {
00285 return response('Invalid page name '.quot($args['page']), 400);
00286 }
00287
00288 $a = expl('.', $args['page']);
00289 $d = CONTENT_DIR.'/'.$a[0];
00290 if (!is_dir($d)) {
00291 $m = umask(0000);
00292 if (!@mkdir($d, 0777)) {
00293 umask($m);
00294 return response('Error creating directory '.quot($d), 500);
00295 }
00296 umask($m);
00297 }
00298
00299 $d .= '/'.$a[1];
00300 if (!is_dir($d)) {
00301 $m = umask(0000);
00302 if (!@mkdir($d, 0777)) {
00303 umask($m);
00304 return response('Error creating directory '.quot($d), 500);
00305 }
00306 umask($m);
00307 }
00308
00309 log_msg('info', 'create_page: created '.quot($args['page']));
00310 invoke_hook('create_page', array('page'=>$args['page']));
00311 return response(true);
00312 }
00313
00314 register_service('glue.create_page', 'create_page', array('auth'=>true));
00315 register_hook('create_page', 'invoked when a page has been created');
00316
00317
00318
00319
00320
00321
00322
00323
00324
00325 function delete_object($args)
00326 {
00327 if (empty($args['name'])) {
00328 return response('Required argument "name" missing or empty', 400);
00329 }
00330 if (!object_exists($args['name'])) {
00331 return response('Object '.quot($args['name']).' does not exist', 404);
00332 }
00333
00334
00335
00336 if (!is_writable(CONTENT_DIR.'/'.str_replace('.', '/', $args['name']))) {
00337 return response('Object '.quot($args['name']).' is read-only, not deleting it', 500);
00338 }
00339
00340
00341
00342
00343 $ret = object_get_symlink($args);
00344 $ret = $ret['#data'];
00345 if ($ret === false) {
00346 $obj = load_object($args);
00347 if ($obj['#error']) {
00348 return $obj;
00349 } else {
00350 $obj = $obj['#data'];
00351 }
00352 invoke_hook('delete_object', array('obj'=>$obj));
00353 }
00354
00355 if (!@unlink(CONTENT_DIR.'/'.str_replace('.', '/', $args['name']))) {
00356 return response('Error deleting object '.quot($args['name']), 500);
00357 } else {
00358 log_msg('info', 'delete_object: deleted '.quot($args['name']));
00359
00360
00361 drop_cache('page');
00362 return response(true);
00363 }
00364 }
00365
00366 register_service('glue.delete_object', 'delete_object', array('auth'=>true));
00367 register_hook('delete_object', 'invoked when an object is going to be deleted, should be used for deleting referenced resources');
00368
00369
00370
00371
00372
00373
00374
00375
00376
00377 function delete_page($args)
00378 {
00379 if (empty($args['page'])) {
00380 return response('Required argument "page" missing or empty', 400);
00381 }
00382 if (!page_exists($args['page'])) {
00383 return response('Page '.quot($args['page']).' does not exist', 404);
00384 }
00385
00386 log_msg('info', 'delete_page: deleting '.quot($args['page']));
00387 invoke_hook('delete_page', array('page'=>$args['page']));
00388
00389
00390
00391
00392
00393 $files = @scandir(CONTENT_DIR.'/'.str_replace('.', '/', $args['page']));
00394 foreach ($files as $f) {
00395 $fn = CONTENT_DIR.'/'.str_replace('.', '/', $args['page']).'/'.$f;
00396 if ($f == '.' || $f == '..') {
00397 continue;
00398 } elseif (substr($f, 0, 1) == '.') {
00399
00400 if (!rm_recursive($fn)) {
00401 log_msg('error', 'delete_page: error deleting '.quot($fn));
00402 }
00403 } elseif (is_link($fn) && !is_file($fn) && !is_dir($fn)) {
00404
00405 if (!@unlink($fn)) {
00406 log_msg('error', 'delete_page: error deleting dangling symlink '.quot($fn));
00407 }
00408 } else {
00409
00410 $ret = delete_object(array('name'=>$args['page'].'.'.$f));
00411 if ($ret['#error']) {
00412 log_msg('error', 'delete_object: '.$ret['#data']);
00413 }
00414 }
00415 }
00416
00417
00418 if (!@rmdir(CONTENT_DIR.'/'.str_replace('.', '/', $args['page']))) {
00419 return response('Error deleting page '.$args['page'], 500);
00420 } else {
00421 log_msg('debug', 'delete_page: deleted '.quot($args['page']));
00422
00423 drop_cache('page', $args['page']);
00424 }
00425
00426
00427 $a = expl('.', $args['page']);
00428
00429
00430 if (!rm_recursive(CONTENT_DIR.'/'.$a[0].'/shared')) {
00431 log_msg('error', 'delete_page: error deleting '.$a.'/shared');
00432 }
00433 @rmdir(CONTENT_DIR.'/'.$a[0].'/shared');
00434 if (@rmdir(CONTENT_DIR.'/'.$a[0])) {
00435 log_msg('info', 'delete_page: parent page directory empty, removing '.quot($a[0]));
00436 }
00437
00438 return response(true);
00439 }
00440
00441 register_service('glue.delete_page', 'delete_page', array('auth'=>true));
00442 register_hook('delete_page', 'invoked when a page is going to be deleted');
00443 register_hook('has_reference', 'used for deleting referenced resources');
00444
00445
00446
00447
00448
00449
00450
00451
00452
00453
00454
00455
00456
00457
00458
00459 function delete_upload($args)
00460 {
00461 if (@is_numeric($args['max_cnt'])) {
00462 $max_cnt = intval($args['max_cnt']);
00463 } else {
00464 $max_cnt = 0;
00465 }
00466
00467 $refs = upload_references(array_merge($args, array('stop_after'=>$max_cnt+1)));
00468 if ($refs['#error']) {
00469 return $refs;
00470 } else {
00471 $refs = $refs['#data'];
00472 }
00473
00474 $f = CONTENT_DIR.'/'.$args['pagename'].'/shared/'.$args['file'];
00475 if (count($refs) <= $max_cnt) {
00476 if (@unlink($f)) {
00477 log_msg('info', 'delete_upload: deleted '.quot($f));
00478
00479 @rmdir(CONTENT_DIR.'/'.$args['pagename'].'/shared');
00480 return response(true);
00481 } else {
00482 return response('Error deleting '.quot($f), 500);
00483 }
00484 } else {
00485 log_msg('info', 'delete_upload: not deleting '.quot($f).' because there are still other objects referencing it');
00486 return response(false);
00487 }
00488 }
00489
00490 register_service('glue.delete_upload', 'delete_upload', array('auth'=>true));
00491
00492
00493
00494
00495
00496
00497
00498
00499
00500 function get_startpage($args)
00501 {
00502 $s = @file_get_contents(CONTENT_DIR.'/startpage');
00503 if ($s) {
00504 return response($s);
00505 } else {
00506 return response(DEFAULT_PAGE);
00507 }
00508 }
00509
00510 register_service('glue.get_startpage', 'get_startpage');
00511
00512
00513
00514
00515
00516
00517
00518
00519
00520 function load_object($args)
00521 {
00522 if (empty($args['name'])) {
00523 return response('Required argument "name" missing or empty', 400);
00524 }
00525
00526
00527 if (($f = @fopen(CONTENT_DIR.'/'.str_replace('.', '/', $args['name']), 'rb')) === false) {
00528 return response('Error opening '.quot($args['name']).' for reading', 404);
00529 }
00530
00531
00532
00533 $ret = array();
00534 $ret['name'] = $args['name'];
00535
00536
00537 $doing_attribs = true;
00538 while (!feof($f)) {
00539 $l = fgets($f, 4096);
00540 if ($doing_attribs) {
00541
00542 if (substr($l, -2) == "\r\n") {
00543 $l = substr($l, 0, -2);
00544 } elseif (substr($l, -1) == "\n") {
00545 $l = substr($l, 0, -1);
00546 } elseif (substr($l, -1) == "\r") {
00547 $l = substr($l, 0, -1);
00548 }
00549 $a = expl(':', $l);
00550 if (count($a) == 0) {
00551 $doing_attribs = false;
00552 } elseif (count($a) == 1) {
00553
00554 } else {
00555 $ret[$a[0]] = implode(':', array_slice($a, 1));
00556 }
00557 } else {
00558
00559 if (isset($ret['content'])) {
00560 $ret['content'] .= $l;
00561 } else {
00562 $ret['content'] = $l;
00563 }
00564 }
00565 }
00566 fclose($f);
00567
00568
00569 $ret['name'] = $args['name'];
00570
00571 return response($ret);
00572 }
00573
00574 register_service('glue.load_object', 'load_object', array('auth'=>true));
00575
00576
00577
00578
00579
00580
00581
00582
00583
00584
00585
00586
00587 function object_get_symlink($args)
00588 {
00589 if (empty($args['name'])) {
00590 return response('Required argument "name" missing or empty', 400);
00591 }
00592
00593
00594 if (is_link(CONTENT_DIR.'/'.str_replace('.', '/', $args['name']))) {
00595 $f = readlink(CONTENT_DIR.'/'.str_replace('.', '/', $args['name']));
00596 if (substr($f, 0, 6) != '../../' || substr($f, 6, 2) == '..') {
00597 log_msg('warn', 'object_get_symlink: target outside of content directory: '.quot($args['name']).' -> '.quot($f));
00598 return response('');
00599 } else {
00600 return response(str_replace('/', '.', substr($f, 6)));
00601 }
00602 } else {
00603 return response(false);
00604 }
00605 }
00606
00607 register_service('glue.object_get_symlink', 'object_get_symlink', array('auth'=>true));
00608
00609
00610
00611
00612
00613
00614
00615
00616
00617 function object_make_symlink($args)
00618 {
00619 if (empty($args['name'])) {
00620 return response('Required argument "name" missing or empty', 400);
00621 }
00622 if (!object_exists($args['name'])) {
00623 return response('Object '.quot($args['name']).' does not exist', 404);
00624 }
00625
00626 $a = expl('.', $args['name']);
00627
00628 $ret = object_get_symlink($args);
00629 $ret = $ret['#data'];
00630 if ($ret !== false && $ret !== '') {
00631
00632 $target = '../../'.str_replace('.', '/', $ret);
00633
00634 $a_target = expl('.', $ret);
00635 $skip_pns = array($a[0], $a_target[0]);
00636 } else {
00637 $target = '../../'.str_replace('.', '/', $args['name']);
00638 $skip_pns = array($a[0]);
00639 }
00640
00641 $pns = pagenames(array());
00642 $pns = $pns['#data'];
00643
00644 foreach ($pns as $pn) {
00645 if (in_array($pn, $skip_pns)) {
00646 continue;
00647 }
00648
00649 if (is_dir(CONTENT_DIR.'/'.$pn.'/head')) {
00650 $link = CONTENT_DIR.'/'.$pn.'/head/'.$a[2];
00651 if (is_file($link) && !is_link($link)) {
00652
00653
00654
00655 delete_object(array('name'=>$pn.'.head.'.$a[2]));
00656 } elseif (is_link($link) && !is_file($link) && !is_dir($link)) {
00657
00658 if (@unlink($link)) {
00659 log_msg('info', 'object_make_symlink: deleted dangling symlink '.quot($pn.'.head.'.$a[2]));
00660 } else {
00661 log_msg('error', 'object_make_symlink: error deleting dangling symlink '.quot($pn.'.head.'.$a[2]));
00662 }
00663 }
00664
00665
00666 if (@symlink($target, $link)) {
00667 log_msg('debug', 'object_make_symlink: '.quot($pn.'.head.'.$a[2]).' -> '.quot($target));
00668
00669 drop_cache('page', $pn.'.head');
00670 }
00671 }
00672 }
00673
00674 return response(true);
00675 }
00676
00677 register_service('glue.object_make_symlink', 'object_make_symlink', array('auth'=>true));
00678
00679
00680
00681
00682
00683
00684
00685
00686
00687
00688
00689
00690 function object_remove_attr($args)
00691 {
00692 if (!isset($args['attr'])) {
00693 return response('Required argument "attr" missing', 400);
00694 }
00695
00696
00697
00698 $_l = _obj_lock($args['name'], LOCK_TIME);
00699 if ($_l === false) {
00700 return response('Could not acquire lock to '.quot($args['name']).' in '.LOCK_TIME.'ms', 500);
00701 }
00702 $obj = load_object($args);
00703 if ($obj['#error']) {
00704
00705 _obj_unlock($_l);
00706 return $obj;
00707 } else {
00708 $obj = $obj['#data'];
00709 }
00710
00711 if (is_array($args['attr'])) {
00712 foreach ($args['attr'] as $a) {
00713 if (isset($obj[$a])) {
00714 unset($obj[$a]);
00715 }
00716 }
00717 } elseif (is_string($args['attr'])) {
00718 if (isset($obj[$args['attr']])) {
00719 unset($obj[$args['attr']]);
00720 }
00721 } else {
00722
00723 _obj_unlock($_l);
00724 return response('Argument "attr" need to be either array or string', 400);
00725 }
00726
00727 $ret = save_object($obj);
00728
00729 _obj_unlock($_l);
00730 return $ret;
00731 }
00732
00733 register_service('glue.object_remove_attr', 'object_remove_attr', array('auth'=>true));
00734
00735
00736
00737
00738
00739
00740
00741
00742 function pagenames($args)
00743 {
00744 if (is_dir(CONTENT_DIR)) {
00745 $files = @scandir(CONTENT_DIR);
00746 $ret = array();
00747 foreach ($files as $f) {
00748 if ($f == '.' || $f == '..' || $f == 'cache' || $f == 'shared') {
00749 continue;
00750 } elseif (!is_dir(CONTENT_DIR.'/'.$f)) {
00751
00752 continue;
00753 } elseif (substr($f, 0, 1) == '.') {
00754
00755 continue;
00756 } else {
00757 $ret[] = $f;
00758 }
00759 }
00760 return response($ret);
00761 } else {
00762 return response(array());
00763 }
00764 }
00765
00766 register_service('glue.pagenames', 'pagenames');
00767
00768
00769
00770
00771
00772
00773
00774
00775
00776
00777
00778
00779
00780 function render_object($args)
00781 {
00782
00783
00784 $obj = load_object($args);
00785 if ($obj['#error']) {
00786 return $obj;
00787 } else {
00788 $obj = $obj['#data'];
00789 }
00790 if (!isset($args['edit'])) {
00791 return response('Required argument "edit" missing', 400);
00792 }
00793 if ($args['edit']) {
00794 $args['edit'] = true;
00795 } else {
00796 $args['edit'] = false;
00797 }
00798
00799 log_msg('debug', 'render_object: rendering '.quot($args['name']));
00800 $ret = invoke_hook_while('render_object', false, array('obj'=>$obj, 'edit'=>$args['edit']));
00801 if (empty($ret)) {
00802 log_msg('warn', 'render_object: nobody claimed '.quot($obj['name']));
00803 return response('');
00804 } else {
00805 $temp = array_keys($ret);
00806 log_msg('debug', 'render_object: '.quot($obj['name']).' was handled by '.quot($temp[0]));
00807 $temp = array_values($ret);
00808
00809 if (0 < strlen($temp[0]) && substr($temp[0], -1) != "\n") {
00810 $temp[0] .= nl();
00811 }
00812 body_append($temp[0]);
00813
00814 return response($temp[0]);
00815 }
00816 }
00817
00818 register_service('glue.render_object', 'render_object');
00819 register_hook('render_object', 'render an object');
00820
00821
00822
00823
00824
00825
00826
00827
00828
00829
00830
00831
00832
00833 function render_page($args)
00834 {
00835
00836
00837 if (empty($args['page'])) {
00838 return response('Required argument "page" missing or empty', 400);
00839 }
00840 if (!page_exists($args['page'])) {
00841 return response('Page '.quot($args['page']).' does not exist', 404);
00842 }
00843 if (!isset($args['edit'])) {
00844 return response('Required argument "edit" missing', 400);
00845 }
00846 if ($args['edit']) {
00847 $args['edit'] = true;
00848 } else {
00849 $args['edit'] = false;
00850 }
00851
00852 log_msg('debug', 'render_page: rendering '.quot($args['page']));
00853 $bdy = &body();
00854 elem_add_class($bdy, 'page');
00855 elem_attr($bdy, 'id', $args['page']);
00856 invoke_hook('render_page_early', array('page'=>$args['page'], 'edit'=>$args['edit']));
00857
00858
00859 $files = @scandir(CONTENT_DIR.'/'.str_replace('.', '/', $args['page']));
00860 foreach ($files as $f) {
00861 $fn = CONTENT_DIR.'/'.str_replace('.', '/', $args['page']).'/'.$f;
00862 if ($f == '.' || $f == '..') {
00863 continue;
00864 } elseif (is_link($fn) && !is_file($fn) && !is_dir($fn)) {
00865
00866 if (@unlink($fn)) {
00867 log_msg('info', 'render_page: deleted dangling symlink '.quot($args['page'].'.'.$f));
00868 } else {
00869 log_msg('error', 'render_page: error deleting dangling symlink '.quot($args['page'].'.'.$f));
00870 }
00871 continue;
00872 }
00873
00874 render_object(array('name'=>$args['page'].'.'.$f, 'edit'=>$args['edit']));
00875 }
00876
00877 invoke_hook('render_page_late', array('page'=>$args['page'], 'edit'=>$args['edit']));
00878 log_msg('debug', 'render_page: finished '.quot($args['page']));
00879
00880
00881 return response(elem_finalize($bdy));
00882 }
00883
00884 register_service('glue.render_page', 'render_page');
00885 register_hook('render_page_early', 'invoked early in the page rendering');
00886 register_hook('render_page_late', 'invoked after the objects have been rendered');
00887
00888
00889
00890
00891
00892
00893
00894
00895
00896 function rename_page($args)
00897 {
00898 if (empty($args['old'])) {
00899 return response('Required argument "old" missing or empty', 400);
00900 }
00901 $pns = pagenames(array());
00902 $pns = $pns['#data'];
00903 if (!in_array($args['old'], $pns)) {
00904 return response('Page name '.quot($args['old']).' does not exist', 404);
00905 }
00906 if (empty($args['new'])) {
00907 return response('Required argument "new" missing or empty', 400);
00908 }
00909 if (in_array($args['new'], $pns)) {
00910 return response('Page name '.quot($args['new']).' already exists', 400);
00911 }
00912 if (!valid_pagename($args['new'].'.head')) {
00913 return response('Invalid page name '.quot($args['new']), 400);
00914 }
00915
00916 if (!@rename(CONTENT_DIR.'/'.$args['old'], CONTENT_DIR.'/'.$args['new'])) {
00917 return response('Error renaming page '.quot($args['old']).' to '.quot($args['new']), 500);
00918 } else {
00919 log_msg('info', 'rename_page: renamed '.quot($args['old']).' to '.quot($args['new']));
00920
00921 $revs = revisions(array('pagename'=>$args['new']));
00922 $revs = $revs['#data'];
00923 foreach ($revs as $rev) {
00924 drop_cache('page', $args['old'].'.'.$rev);
00925 }
00926 invoke_hook('rename_page', array('pagename'=>$args['new']));
00927 return response(true);
00928 }
00929 }
00930
00931 register_service('glue.rename_page', 'rename_page', array('auth'=>true));
00932 register_hook('rename_page', 'invoked when a page has been renamed');
00933
00934
00935
00936
00937
00938
00939
00940
00941
00942
00943 function copy_page($args)
00944 {
00945 if (empty($args['old'])) {
00946 return response('Required argument "old" missing or empty', 400);
00947 }
00948 $pns = pagenames(array());
00949 $pns = $pns['#data'];
00950 if (!in_array($args['old'], $pns)) {
00951 return response('Page name '.quot($args['old']).' does not exist', 404);
00952 }
00953 if (empty($args['new'])) {
00954 return response('Required argument "new" missing or empty', 400);
00955 }
00956 if (in_array($args['new'], $pns)) {
00957 return response('Page name '.quot($args['new']).' already exists', 400);
00958 }
00959 if (!valid_pagename($args['new'].'.head')) {
00960 return response('Invalid page name '.quot($args['new']), 400);
00961 }
00962
00963 $src = CONTENT_DIR.'/'.$args['old'];
00964 $dest = CONTENT_DIR.'/'.$args['new'];
00965
00966 $dirs = scandir($src);
00967 foreach ($dirs as $d) {
00968
00969 if ($d == '.' || $d == '..' || substr($d, 0, 5) == 'auto-') {
00970 continue;
00971
00972 } elseif (is_dir($src.'/'.$d)) {
00973 $m = umask(0000);
00974 if (!@mkdir($dest.'/'.$d, 0777, true)) {
00975 umask($m);
00976 return response('Error creating directory '.quot($dest.'/'.$d), 500);
00977 }
00978 umask($m);
00979 $files = scandir($src.'/'.$d);
00980 foreach ($files as $f) {
00981 if ($f == '.' || $f == '..') {
00982 continue;
00983 } elseif (is_file($src.'/'.$d.'/'.$f)) {
00984
00985 $m = umask(0111);
00986 if (!@copy($src.'/'.$d.'/'.$f, $dest.'/'.$d.'/'.$f)) {
00987 log_msg('error', 'copy: error copying '.quot($src.'/'.$d.'/'.$f).' to '.quot($dest.'/'.$d.'/'.$f).', skipping file');
00988 }
00989 umask($m);
00990 }
00991 }
00992 }
00993 }
00994 log_msg('info', 'copy_page: copied '.quot($args['old']).' to '.quot($args['new']));
00995 invoke_hook('copy_page', array('pagename'=>$args['new']));
00996 return response(true);
00997 }
00998
00999 register_service('glue.copy_page', 'copy_page', array('auth'=>true));
01000 register_hook('copy_page', 'invoked when a page has been copied');
01001
01002
01003
01004
01005
01006
01007
01008
01009
01010
01011 function revert($args)
01012 {
01013 if (empty($args['page'])) {
01014 return response('Required argument "page" missing or empty', 400);
01015 }
01016 if (!page_exists($args['page'])) {
01017 return response('Page '.quot($args['page']).' does not exist', 404);
01018 }
01019 $a = expl('.', $args['page']);
01020 if ($a[1] == 'head') {
01021 return response('Cannot revert to head revision', 400);
01022 }
01023
01024 log_msg('info', 'revert: reverting to '.quot($args['page']));
01025
01026
01027
01028 if (page_exists($a[0].'.head')) {
01029 $ret = delete_page(array('page'=>$a[0].'.head'));
01030 if ($ret['#error']) {
01031 return $ret;
01032 }
01033 }
01034
01035
01036 $dest = CONTENT_DIR.'/'.$a[0].'/head';
01037 $m = umask(0000);
01038 if (!@mkdir($dest, 0777)) {
01039 umask($m);
01040 return response('Error creating directory '.quot($dest), 500);
01041 }
01042 umask($m);
01043
01044
01045 $src = CONTENT_DIR.'/'.$a[0].'/'.$a[1];
01046 $files = scandir($src);
01047 foreach ($files as $f) {
01048 if ($f == '.' || $f == '..') {
01049 continue;
01050 } elseif (is_file($src.'/'.$f)) {
01051
01052 $m = umask(0111);
01053 if (!@copy($src.'/'.$f, $dest.'/'.$f)) {
01054 log_msg('error', 'revert: error copying '.quot($src.'/'.$f).' to '.quot($dest.'/'.$f).', skipping file');
01055 }
01056 umask($m);
01057 }
01058 }
01059
01060 log_msg('info', 'revert: reverted to '.quot($args['page']));
01061 invoke_hook('revert', array('page'=>$args['page']));
01062
01063 return response(true);
01064 }
01065
01066 register_service('glue.revert', 'revert', array('auth'=>true));
01067 register_hook('revert', 'invoked after a page has been reverted to');
01068
01069
01070
01071
01072
01073
01074
01075
01076
01077 function revisions($args)
01078 {
01079 if (empty($args['pagename'])) {
01080 return response('Required argument "pagename" missing or empty', 400);
01081 }
01082 if (!is_dir(CONTENT_DIR.'/'.$args['pagename'])) {
01083 return response('Page name '.quot($args['pagename']).' does not exist', 404);
01084 }
01085
01086 $files = @scandir(CONTENT_DIR.'/'.$args['pagename']);
01087 $ret = array();
01088 foreach ($files as $f) {
01089 if ($f == '.' || $f == '..' || $f == 'shared') {
01090 continue;
01091 } elseif (!is_dir(CONTENT_DIR.'/'.$args['pagename'].'/'.$f)) {
01092
01093 continue;
01094 } elseif (substr($f, 0, 1) == '.') {
01095
01096 continue;
01097 } else {
01098 $ret[] = $f;
01099 }
01100 }
01101 return response($ret);
01102 }
01103
01104 register_service('glue.revisions', 'revisions');
01105
01106
01107
01108
01109
01110
01111
01112
01113
01114
01115
01116 function revisions_info($args)
01117 {
01118 $revs = revisions($args);
01119 if ($revs['#error']) {
01120 return $revs;
01121 }
01122
01123 $ret = array();
01124 foreach ($revs['#data'] as $r) {
01125 $d = CONTENT_DIR.'/'.$args['pagename'].'/'.$r;
01126 $ret[] = array('revision'=>$r, 'time'=>@filemtime($d), 'num_objs'=>count(@scandir($d))-2, 'page'=>$args['pagename'].'.'.$r);
01127 }
01128
01129 if (isset($args['sort']) && $args['sort'] == 'time') {
01130
01131 $head = false;
01132 for ($i=0; $i < count($ret); $i++) {
01133 if ($ret[$i]['revision'] == 'head') {
01134 $head = $ret[$i];
01135 array_splice($ret, $i, 1);
01136 $i--;
01137 }
01138 }
01139 usort($ret, '_cmp_time');
01140 if ($head !== false) {
01141 $ret = array_merge(array($head), $ret);
01142 }
01143 }
01144
01145 return response($ret);
01146 }
01147
01148 register_service('glue.revisions_info', 'revisions_info');
01149
01150
01151
01152
01153
01154
01155
01156
01157
01158
01159
01160
01161
01162 function save_object($args)
01163 {
01164 if (empty($args['name'])) {
01165 return response('Required argument "name" missing or empty', 400);
01166 }
01167
01168
01169 $m = umask(0111);
01170 if (($f = @fopen(CONTENT_DIR.'/'.str_replace('.', '/', $args['name']), 'wb')) === false) {
01171 umask($m);
01172 return response('Error opening '.quot($args['name']).' for writing', 500);
01173 }
01174 umask($m);
01175
01176
01177 foreach ($args as $key=>$val) {
01178 if ($key == 'name' || $key == 'content') {
01179 continue;
01180 }
01181
01182 if (strpos($key, ':') !== false) {
01183 log_msg('warn', 'save_object: skipping attribute '.quot($key).' in object '.quot($args['name']));
01184 continue;
01185 }
01186
01187 $val = str_replace("\r\n", '', $val);
01188 $val = str_replace("\n", '', $val);
01189 $val = str_replace("\r", '', $val);
01190 fwrite($f, $key.':'.$val."\n");
01191 }
01192
01193
01194 if (isset($args['content'])) {
01195 fwrite($f, "\n");
01196 fwrite($f, $args['content']);
01197 }
01198
01199 fclose($f);
01200
01201 drop_cache('page');
01202
01203 return response(true);
01204 }
01205
01206 register_service('glue.save_object', 'save_object', array('auth'=>true));
01207
01208
01209
01210
01211
01212
01213
01214
01215
01216
01217
01218 function save_state($args)
01219 {
01220 if (empty($args['html'])) {
01221 return response('Required argument "html" missing or empty', 400);
01222 }
01223
01224 require_once('html.inc.php');
01225 require_once('html_parse.inc.php');
01226
01227 $elem = html_parse_elem($args['html']);
01228 if (!elem_has_class($elem, 'object')) {
01229 return response('Error saving state as class "object" is not set', 400);
01230 } elseif (!object_exists(elem_attr($elem, 'id'))) {
01231 return response('Error saving state as object does not exist', 404);
01232 }
01233
01234
01235 $L = _obj_lock(elem_attr($elem, 'id'), LOCK_TIME);
01236 if ($L === false) {
01237 return response('Could not acquire lock to '.quot($args['name']).' in '.LOCK_TIME.'ms', 500);
01238 }
01239 $obj = load_object(array('name'=>elem_attr($elem, 'id')));
01240 if ($obj['#error']) {
01241
01242 _obj_unlock($L);
01243 return response('Error saving state, cannot load '.quot(elem_attr($elem, 'id')), 500);
01244 } else {
01245 $obj = $obj['#data'];
01246 }
01247 $ret = invoke_hook_while('save_state', false, array('elem'=>$elem, 'obj'=>$obj));
01248
01249 _obj_unlock($L);
01250 if (count($ret) == 0) {
01251 return response('Error saving state as nobody claimed element', 500);
01252 } else {
01253 $temp = array_keys($ret);
01254 log_msg('info', 'save_state: '.quot($obj['name']).' was handled by '.quot($temp[0]));
01255 return response(true);
01256 }
01257 }
01258
01259 register_service('glue.save_state', 'save_state', array('auth'=>true));
01260
01261
01262
01263 register_hook('save_state', 'save the current state of an object to disk');
01264
01265
01266
01267
01268
01269
01270
01271
01272
01273
01274 function set_startpage($args)
01275 {
01276 if (!isset($args['page'])) {
01277 return response('Required argument "page" missing', 400);
01278 }
01279
01280 $m = umask(0111);
01281 if (@file_put_contents(CONTENT_DIR.'/startpage', $args['page']) === false) {
01282 umask($m);
01283 return response('Error setting start page', 500);
01284 } else {
01285 umask($m);
01286 return response(true);
01287 }
01288 }
01289
01290 register_service('glue.set_startpage', 'set_startpage', array('auth'=>true));
01291
01292
01293
01294
01295
01296
01297
01298
01299
01300
01301
01302
01303
01304 function snapshot($args)
01305 {
01306 if (empty($args['page'])) {
01307 return response('Required argument "page" missing or empty', 400);
01308 }
01309 if (!page_exists($args['page'])) {
01310 return response('Page '.quot($args['page']).' does not exist', 404);
01311 }
01312
01313 $a = expl('.', $args['page']);
01314 if (empty($args['rev'])) {
01315 $args['rev'] = 'auto-'.date('YmdHis');
01316 } elseif (page_exists($a[0].'.'.$args['rev'])) {
01317 return response('Revision '.quot($args['rev']).' already exists', 400);
01318 } elseif (!valid_pagename($a[0].'.'.$args['rev'])) {
01319 return response('Invalid revision '.quot($args['rev']), 400);
01320 }
01321
01322
01323 $dest = CONTENT_DIR.'/'.$a[0].'/'.$args['rev'];
01324 $m = umask(0000);
01325 if (!@mkdir($dest)) {
01326 umask($m);
01327 return response('Error creating directory '.quot($dest), 500);
01328 }
01329 umask($m);
01330
01331
01332
01333 $src = CONTENT_DIR.'/'.str_replace('.', '/', $args['page']);
01334 $files = scandir($src);
01335 foreach ($files as $f) {
01336 if ($f == '.' || $f == '..') {
01337 continue;
01338 } elseif (is_dir($src.'/'.$f) && substr($f, 0, 1) == '.') {
01339
01340 continue;
01341 } elseif (is_dir($src.'/'.$f)) {
01342 log_msg('warn', 'snapshot: skipping '.quot($src.'/'.$f).' as we don\'t support directories inside pages');
01343 } elseif (is_link($src.'/'.$f) && is_file($src.'/'.$f)) {
01344
01345 $s = @file_get_contents($src.'/'.$f);
01346 $m = umask(0111);
01347 if (!@file_put_contents($dest.'/'.$f, $s)) {
01348 log_msg('error', 'snapshot: error writing to '.quot($dest.'/'.$f). ', skipping file');
01349 } else {
01350 log_msg('debug', 'snapshot: copied the content of symlink '.quot($args['page'].'.'.$f));
01351 }
01352 umask($m);
01353
01354
01355 $dest_name = $a[0].'.'.$args['rev'].'.'.$f;
01356 $dest_obj = load_object(array('name'=>$dest_name));
01357 if ($dest_obj['#error']) {
01358 log_msg('error', 'snapshot: error loading snapshotted object '.quot($dest_name).', skipping hook');
01359 } else {
01360 $dest_obj = $dest_obj['#data'];
01361
01362 $src_name = $args['page'].'.'.$f;
01363 $src_target = object_get_symlink(array('name'=>$src_name));
01364 if ($src_target['#error']) {
01365 log_msg('error', 'snapshot: error getting the symlink target of source object '.quot($src_name).', skipping hook');
01366 } else {
01367 $src_target = $src_target['#data'];
01368
01369 invoke_hook('snapshot_symlink', array('obj'=>$dest_obj, 'origin'=>implode('.', array_slice(expl('.', $src_target), 0, 2))));
01370 }
01371 }
01372 } elseif (is_file($src.'/'.$f)) {
01373
01374 $m = umask(0111);
01375 if (!@copy($src.'/'.$f, $dest.'/'.$f)) {
01376 log_msg('error', 'snapshot: error copying '.quot($src.'/'.$f).' to '.quot($dest.'/'.$f).', skipping file');
01377 }
01378 umask($m);
01379 }
01380 }
01381
01382 log_msg('info', 'snapshot: created snapshot '.quot($a[0].'.'.$args['rev']).' from '.quot($args['page']));
01383 return response($a[0].'.'.$args['rev']);
01384 }
01385
01386 register_service('glue.snapshot', 'snapshot', array('auth'=>true));
01387 register_hook('snapshot_symlink', 'invoked when a symlink is part of a page that gets snapshotted; the module in question is supposed to copy all referenced files to the shared directory of the destination page');
01388
01389
01390
01391
01392
01393
01394
01395
01396
01397
01398
01399
01400
01401
01402 function update_object($args)
01403 {
01404 if (empty($args['name'])) {
01405 return response('Required argument "name" missing or empty', 400);
01406 }
01407
01408
01409 $L = _obj_lock($args['name'], LOCK_TIME);
01410
01411
01412 if ($L === false) {
01413 return response('Could not acquire lock to '.quot($args['name']).' in '.LOCK_TIME.'ms', 500);
01414 }
01415 $old = load_object($args);
01416 if ($old['#error']) {
01417 $old = array();
01418 } else {
01419 $old = $old['#data'];
01420 }
01421 $new = array_merge($old, $args);
01422
01423 $ret = save_object($new);
01424
01425 _obj_unlock($L);
01426 return $ret;
01427 }
01428
01429 register_service('glue.update_object', 'update_object', array('auth'=>true));
01430
01431
01432
01433
01434
01435
01436
01437
01438
01439
01440
01441
01442 function upload_files($args)
01443 {
01444 if (empty($args['page'])) {
01445 return response('Required argument "page" missing or empty', 400);
01446 }
01447 if (!page_exists($args['page'])) {
01448 return response('Page '.quot($args['page']).' does not exist', 404);
01449 }
01450
01451 $ret = array();
01452
01453 log_msg('debug', 'upload_files: $_FILES is '.var_dump_inl($_FILES));
01454 foreach ($_FILES as $f) {
01455 $existed = false;
01456 $fn = upload_file($f['tmp_name'], $args['page'], $f['name'], $existed);
01457 if ($fn === false) {
01458 continue;
01459 } else {
01460 $args = array_merge($args, array('file'=>$fn, 'mime'=>$f['type'], 'size'=>$f['size']));
01461
01462 if ($args['mime'] == 'application/octet-stream') {
01463 $args['mime'] = '';
01464 }
01465 }
01466 $s = false;
01467
01468 if (!empty($args['preferred_module'])) {
01469
01470 load_modules();
01471 $func = $args['preferred_module'].'_upload';
01472 if (is_callable($func)) {
01473 log_msg('debug', 'upload_files: invoking hook upload, calling '.$func);
01474 $s = $func($args);
01475 if ($s !== false) {
01476 log_msg('info', 'upload_object: '.quot($fn).' was handled by '.quot($args['preferred_module']));
01477 }
01478 }
01479 }
01480
01481 if ($s === false) {
01482 $r = invoke_hook_while('upload', false, $args);
01483 if (count($r) == 1) {
01484 $s = array_pop(array_values($r));
01485 log_msg('info', 'upload_object: '.quot($fn).' was handled by '.quot(array_pop(array_keys($r))));
01486 }
01487 }
01488
01489 if ($s === false) {
01490 $r = invoke_hook_while('upload_fallback', false, $args);
01491 if (count($r) == 1) {
01492 $s = array_pop(array_values($r));
01493 log_msg('info', 'upload_object: '.quot($fn).' was (fallback-) handled by '.quot(array_pop(array_keys($r))));
01494 }
01495 }
01496
01497 if ($s === false) {
01498 log_msg('warn', 'upload_files: nobody cared about file '.quot($fn).', type '.$f['type']);
01499
01500 if (!$existed) {
01501 $a = expl('.', $args['page']);
01502 @unlink(CONTENT_DIR.'/'.$a[0].'/shared/'.$fn);
01503 }
01504 } else {
01505 $ret[] = $s;
01506 }
01507 }
01508
01509 return response($ret);
01510 }
01511
01512 register_service('glue.upload_files', 'upload_files', array('auth'=>true));
01513
01514
01515
01516
01517
01518
01519
01520
01521
01522
01523
01524
01525 function upload_references($args)
01526 {
01527 $revs = revisions($args);
01528 if ($revs['#error']) {
01529 return $revs;
01530 } else {
01531 $revs = $revs['#data'];
01532 }
01533 if (empty($args['file'])) {
01534 return response('Required argument "file" missing or empty', 400);
01535 }
01536
01537 if (@is_numeric($args['stop_after'])) {
01538 $stop_after = intval($args['stop_after']);
01539 } else {
01540 $stop_after = 0;
01541 }
01542
01543 $ret = array();
01544
01545
01546 foreach ($revs as $rev) {
01547
01548 $files = @scandir(CONTENT_DIR.'/'.$args['pagename'].'/'.$rev);
01549 foreach ($files as $f) {
01550 if ($f == '.' || $f == '..') {
01551 continue;
01552 }
01553 $obj = load_object(array('name'=>$args['pagename'].'.'.$rev.'.'.$f));
01554 if ($obj['#error']) {
01555 continue;
01556 } else {
01557 $obj = $obj['#data'];
01558 }
01559
01560 log_msg('debug', 'upload_references: checking '.quot($obj['name']));
01561 $revs = invoke_hook_while('has_reference', false, array('file'=>$args['file'], 'obj'=>$obj));
01562 if (count($revs)) {
01563 $ret[] = $args['pagename'].'.'.$rev.'.'.$f;
01564 if (count($ret) == $stop_after) {
01565
01566 return response($ret);
01567 }
01568 }
01569 }
01570 }
01571
01572 return response($ret);
01573 }
01574
01575 register_service('glue.upload_references', 'upload_references', array('auth'=>true));
01576 register_hook('has_reference', 'check if an object references an uploaded file');