1<?php
2  /**
3   * @license    http://www.cecill.info/licences/Licence_CeCILL-B_V1-fr.html
4   * @author     Francois Merciol <dokuplugin@merciol.fr>
5   *
6   * Plugin Glossary: manage forms for glossary
7   */
8if(!defined('DOKU_INC'))
9  define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
10if(!defined('DOKU_PLUGIN'))
11  define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
12
13/* Sort glossary by definition */
14function cmpGlossary ($a, $b) {
15  if ($a['word'] == $b['word'])
16    return 0;
17  return ($a['word'] < $b['word']) ? -1 : 1;
18}
19
20class glossary {
21
22  // ============================================================
23  // Config attributs
24  // ============================================================
25  var $cacheRootDir;	// root cache directory
26  var $cacheDir;	// cache directory for specific namespace
27  var $dataRootDir;	// root data directory
28  var $dataDir;		// data directory for specific namespace for xml file
29  var $recentDays;	// time during the clock is display to indicate a nex definition
30  var $maxIP;		// max proposition per guest user
31  var $propGroup;	// user group without limits of proposition
32  var $adminGroup;	// admin group who validate proposition
33  var $transSep;	// line separator for translate if not empty
34
35  // ============================================================
36  // Constant attributs
37  // ============================================================
38  var $configFile   = "config.xml";	// file config name
39  var $prop         = "prop-";		// prefix for proposal
40  var $def          = "def-";		// prefix for definition // XXXX
41  var $poll         = "poll-";		// prefix for polling (evaluation per definition)
42  var $form         = "form-";		// subset used in form
43  var $hide         = "hide-";		// subset not used in form
44  var $statusFields =			// fields managed per step (status)
45    array ("prop-" => array ("date", "ip", "email", "ns", "ticket", "word", "translate", "why"),
46	   "def-"  => array ("date", "ip", "email", "ns", "ticket", "useTicket", "word", "translate"),
47	   "poll-" => array ("word", "view", "up", "down"),
48	   "form-" => array ("ticket", "useTicket", "word", "translate", "why"),
49	   "hide-" => array ("date", "ip", "email", "ns"));
50  var $oddEven      =			// for row color
51    array ("odd", "even");
52  var $imgDir;
53
54  // ============================================================
55  // Transcient attributs
56  // ============================================================
57  var $message = array ();	// "notify" =>, "info" =>, "success" =>, "error" =>
58  var $plugin;			// link to wiki plugin information (conf, lang, ...)
59  var $NS;			// namespace where definition could be found
60  var $lastNotification;	// time of last proposal notification
61  var $lastNotificationReset;	// time of last administrators acknowledgement
62  var $md5ns;			// the ns directory
63  var $md5id;			// the wiki id
64  var $sampleFile;		// cache for sample
65  var $listFile;		// cache for sample
66
67  // ============================================================
68  function createDirIsNeeded ($dir) {
69    if (is_dir ($dir))
70      return;
71    @mkdir ($dir);
72    @chmod ($dir, 0775);
73  }
74  function __construct ($plugin, $ns) {
75    $this->plugin = $plugin;
76    $this->imgDir = DOKU_REL.'lib/plugins/glossary/images/';
77    global $conf;
78    $savedir = ((!$conf['savedir'] || strpos ($conf['savedir'], '.') === 0) ? DOKU_INC : "").$conf['savedir']."/";
79    $this->cacheRootDir = $savedir."cache/glossary/";
80    $this->dataRootDir = $savedir.trim ($this->plugin->getConf ('dataDir').'/');
81    glossary::createDirIsNeeded ($this->cacheRootDir);
82    glossary::createDirIsNeeded ($this->dataRootDir);
83    $this->NS = cleanId (trim ($ns));
84    $this->md5ns = md5 ($this->NS);
85    $this->cacheDir = $this->cacheRootDir.$this->md5ns."/";
86    glossary::createDirIsNeeded ($this->cacheDir);
87    $this->dataDir = $this->dataRootDir.$this->md5ns."/";
88    glossary::createDirIsNeeded ($this->dataDir);
89    $this->sampleFile = $this->cacheDir."sample.cache";
90    $this->listFile = $this->cacheDir."list.cache";
91    $this->recentDays = $this->getConf ('recentDays');
92    $this->maxIP = $this->getConf ('maxIP');
93    $this->adminGroup = trim ($this->getConf ('adminGroup'));
94    $this->propGroup = trim ($this->getConf ('propGroup'));
95    $this->transSep = trim ($this->getConf ('transSep'));
96  }
97
98  function getConf ($name) {
99    return $this->plugin->getConf ($name);
100  }
101  function getLang ($name) {
102    return $this->plugin->getLang ($name);
103  }
104  // function isAdmin ($name) {
105  //   return $this->plugin->isAdmin ($name);
106  // }
107  function localFN ($name) {
108    return $this->plugin->localFN ($name);
109  }
110
111  /* messages container to be display before plugin */
112  function message ($type, $text) {
113    if (isset ($this->message[$type]))
114      $this->message[$type] .= '<br/>';
115    $this->message[$type] .= $text;
116  }
117  /* debug messages for admin only */
118  function debug ($text) {
119    global $INFO;
120    if (in_array ('admin', $INFO ['userinfo'] ['grps']))
121      $this->message ('notify', '<pre>'.$text.'</pre>');
122  }
123  /* get next parity for row color */
124  function nextOddEven (&$even) {
125    $result = $this->oddEven [$even];
126    $even = 1 - $even;
127    return $result;
128  }
129
130  // ============================================================
131  // Control rights
132  // ============================================================
133  /* true if the user has the admin rights */
134  function testAdminGroup () {
135    global $INFO;
136    return
137      isset ($INFO ['userinfo']['grps']) &&
138      in_array ($this->adminGroup, $INFO ['userinfo']['grps']);
139  }
140
141  /* true if the user has no proposition limits */
142  function testPropGroup () {
143    global $INFO;
144    return
145      isset ($INFO ['userinfo']['grps']) &&
146      in_array ($this->propGroup, $INFO ['userinfo']['grps']);
147  }
148
149  /* true if limit occured (i.e. problems => can't continued)*/
150  function maxIP (&$request) {
151    $ip = $request['ip'];
152    $count = 1;
153    $all = $this->readAllGlossary ($this->prop);
154    if (isset ($all[$this->md5id])) {
155      $this->message ('success', $this->getLang ('update'));
156      return false;
157    }
158
159    if ($this->testPropGroup ())
160      return false;
161
162    foreach ($all as $record)
163      if ($record['ip'] == $ip)
164	$count++;
165    if ($count <= $this->maxIP) {
166      if ($count == $this->maxIP)
167	$this->message ('notify', $this->getLang ('lastIP'));
168      return false;
169    }
170    $this->message ('error', $this->getLang ('maxIP'));
171    return true;
172  }
173
174  // ============================================================
175  // Control fields
176  // ============================================================
177  function testNotEmpty () {
178    return
179      $this->wordOk () || $this->translateOk () || $this->translateOk () || $this->ticketOk ();
180  }
181  function wordOk () {
182    return isset ($_REQUEST ['glossary']['word']) && trim ($_REQUEST ['glossary']['word']) != "";
183  }
184  function translateOk () {
185    return isset ($_REQUEST ['glossary']['translate']) && trim ($_REQUEST ['glossary']['translate']) != "";
186  }
187  function ticketOk () {
188    return isset ($_REQUEST ['glossary']['ticket']) && trim ($_REQUEST ['glossary']['ticket']) != "";
189  }
190
191  // ============================================================
192  // Manage XML file
193  // ============================================================
194  /* read lodging config */
195  function readConfig ($dir) {
196    $fileName = $dir.$this->configFile;
197    if (!file_exists ($fileName))
198      return false;
199    $xml = new DOMDocument ("1.0", "utf8");
200    $xml->load ($fileName);
201    $root = $xml->documentElement;
202    $this->lastNotification = $root->getElementsByTagName ("lastNotification")->item (0)->nodeValue;
203    $this->lastNotificationReset = $root->getElementsByTagName ("lastNotificationReset")->item (0)->nodeValue;
204    return $root->getElementsByTagName ("nameSpace")->item (0)->nodeValue;
205  }
206  /* write lodging config */
207  function writeConfig () {
208    if ($this->NS == false)
209      return;
210    $fileName = $this->dataDir.$this->configFile;
211    @mkdir ($this->dataDir);
212    @chmod ($this->dataDir, 0775);
213    $xml = new DOMDocument ("1.0", "utf8");
214    $root = $xml->createElement ("glossary");
215    $xml->appendChild ($root);
216    $root->appendChild ($xml->createElement ("nameSpace", htmlspecialchars ($this->NS)));
217    $root->appendChild ($xml->createElement ("lastNotification", htmlspecialchars ($this->lastNotification)));
218    $root->appendChild ($xml->createElement ("lastNotificationReset", htmlspecialchars ($this->lastNotificationReset)));
219    $xml->formatOutput = true;
220    $xml->save ($fileName);
221    chmod ($fileName, 0664);
222  }
223
224  /* read all propositions, definitions, polls */
225  function readAllGlossary ($status) {
226    if (!is_dir ($this->dataDir))
227      return;
228    if ($this->NS == false)
229      return;
230    $result = array ();
231    $exclude_array = explode ("|", ".|..|".$this->configFile);
232    $pathDirObj = opendir ($this->dataDir);
233    while (false !== ($file = readdir ($pathDirObj))) {
234        if (in_array (strtolower ($file), $exclude_array) || !preg_match ('#'.$status.'(.*)\.xml$#i', $file, $regs))
235	continue;
236      $result[$regs[1]] = $this->readGlossary ($regs[1], $status, LOCK_SH);
237    }
238    uasort ($result, "cmpGlossary");
239    return $result;
240  }
241
242  /* read one proposition, definition, poll */
243  function readGlossary ($md5id, $status, $lock) {
244    $fileName = $this->dataDir.$status.$md5id.".xml";
245    if (!file_exists ($fileName))
246      return false;
247    $lock = LOCK_SH;
248    if ($lock != LOCK_SH) {
249      $fp = fopen ($fileName, "r+");
250      if (!flock ($fp, $lock)) {
251	$this->message ("error", "can't lock file ".$fileName.".");
252	return;
253      }
254    }
255    $xml = new DOMDocument ("1.0", "utf8");
256    $xml->load ($fileName);
257    $root = $xml->documentElement;
258    $result = array ();
259    foreach ($this->statusFields [$status] as $field)
260      $result [$field] = $root->getElementsByTagName ($field)->item (0)->nodeValue;
261    return $result;
262  }
263  /* write one proposition, definition, poll */
264  function writeGlossary ($md5id, &$values, $status) {
265    if (! isset ($values['ns']) || $values['ns'] == "")
266      $values['ns'] = $this->NS; // XXX compatibilité
267    $xml = new DOMDocument ("1.0", "utf8");
268    $root = $xml->createElement ("glossary");
269    $xml->appendChild ($root);
270    foreach ($this->statusFields [$status] as $field)
271      $root->appendChild ($xml->createElement ($field, htmlspecialchars ($values [$field])));
272    $xml->formatOutput = true;
273    $fileName = $this->dataDir.$status.$md5id.".xml";
274    $xml->save ($fileName);
275    chmod ($fileName, 0664);
276    //flock (fopen ($fileName, "r+"), LOCK_UN);
277  }
278  /* remove one proposition, definition, poll */
279  function removeGlossary ($md5id, $status) {
280    $fileName = $this->dataDir.$status.$md5id.".xml";
281    @unlink ($fileName);
282  }
283
284  /* count file propositions, definitions, polls */
285  function getGlosarySize ($status, $md5ns) {
286    $subDir = $this->dataRootDir.$md5ns."/";
287    if (!is_dir ($subDir))
288      return 0;
289    $result = 0;
290    $exclude_array = explode ("|", ".|..|".$this->configFile);
291    $pathDirObj = opendir ($subDir);
292    while (false !== ($file = readdir ($pathDirObj))) {
293      if (in_array (strtolower ($file), $exclude_array) || !eregi ($status.'(.*)\.xml$', $file, $regs))
294	continue;
295      $result++;
296    }
297    return $result;
298  }
299
300  // ============================================================
301  // Actions to performed
302  // ============================================================
303  /* update proposition, definition */
304  function updateRequest (&$request, $status) {
305    $reread = false;
306    $update = false;
307    $this->md5id = md5 (trim ($request['ticket']));
308    $oldValues = $this->readGlossary ($this->md5id, $status, LOCK_SH);
309    if ($oldValues) {
310      foreach ($this->statusFields [$this->form] as $field) {
311	if ((!isset ($request[$field]) || $request[$field] == "") &&
312	    $request[$field] != $oldValues [$field]) {
313	  $request[$field] = $oldValues [$field];
314	  $reread = true;
315	} elseif (isset ($request[$field]) && $request[$field] != "" &&
316		  $request[$field] != $oldValues [$field]) {
317	  $update = true;
318	}
319      }
320      foreach ($this->statusFields [$this->hide] as $field)
321	$request[$field] = $oldValues [$field];
322      if ($reread)
323	$this->message ('success', $this->getLang ('readData'));
324      return $update;
325    }
326    return true;
327  }
328
329  /* display poll */
330  function printPoll ($arg) {
331    echo
332      '<div class="glossary toolTip">'.NL;
333    $this->printPollAjax (md5 (trim ($arg)));
334    echo '</div>';
335  }
336
337  function printPollAjax ($md5id) {
338    $filename = $this->cacheDir."poll-".md5($md5id).".cache";
339    if (file_exists ($filename)) {
340      echo file_get_contents ($filename);
341      return;
342    }
343    $poll = $this->readGlossary ($md5id, $this->poll, LOCK_SH);
344    list ($scoreVal, $scoreImg) = $this->getScore ($poll);
345    $text =
346      '<a onClick="glossaryPoll(this,\''.$md5id.'\',\'down\',\''.$this->NS.'\');">'.NL.
347      '  <img src="'.$this->imgDir.'face-sad.png" /><span>'.$this->getLang ('tipPollDown').'</span>'.NL.
348      '</a>'.
349      $scoreImg.NL.
350      '<a onClick="glossaryPoll(this,\''.$md5id.'\',\'up\',\''.$this->NS.'\');">'.NL.
351      '  <img src="'.$this->imgDir.'face-smile.png" /><span>'.$this->getLang ('tipPollUp').'</span>'.NL.
352      '</a>';
353    if (!$poll|| ($poll['up']+$poll['down'] < 1))
354      $text .=
355	'<br/><b>'.$this->getLang ('notPolledYet').'</b>';
356    file_put_contents ($filename, $text);
357    echo $text;
358  }
359
360  function getScore ($poll) {
361      $scoreImg = '<img src="'.$this->imgDir.'score';
362      if ($poll && $poll["up"] + $poll["down"] > 0) {
363	$scoreVal = (($poll["up"] - $poll["down"]) /
364		     (max (5, $poll["up"] + $poll["down"])));
365	$scoreImg .= '-'.intval (round (($scoreVal+1)*3));
366      } else
367	$scoreVal = 0;
368      $scoreImg .= '.png"/>';
369      return array ($scoreVal, $scoreImg);
370  }
371
372  /* update poll */
373  function poll () {
374    $request = &$_REQUEST ['glossary'];
375    $md5id = $request['ticket'];
376    $opinion = $request['opinion'];
377    $all = $this->readAllGlossary ($this->def);
378    if (! isset ($all[$md5id]) || !in_array ($opinion, $this->statusFields [$this->poll])) {
379      $this->message ('error', $this->getLang ('noDef'));
380      return;
381    }
382    $poll = $this->readGlossary ($md5id, $this->poll, LOCK_EX);
383    if (!$poll)
384      $poll = array ("word" => $all[$md5id]['word'], "up" => 0, "down" => 0);
385    if ($poll [$opinion] != PHP_INT_MAX)
386      $poll [$opinion]++;
387    $this->writeGlossary ($md5id, $poll, $this->poll);
388    $this->message ('success', $this->getLang ('writePoll'));
389    @unlink ($this->cacheDir."poll-".md5($md5id).".cache");
390    $this->printPollAjax ($md5id);
391  }
392
393
394  function incView ($md5id, $word) {
395    $poll = $this->readGlossary ($md5id, $this->poll, LOCK_EX);
396    if (!$poll)
397      $poll = array ("word" => $word, "view" => 0, "up" => 0, "down" => 0);
398    $poll ["view"]++;
399    $this->writeGlossary ($md5id, $poll, $this->poll);
400    return $poll;
401  }
402
403  function clearCache ($nsMd5) {
404    $exclude = ".|..";
405    $exclude_array = explode("|", $exclude);
406    $allNsMd5 = array ();
407    if ($nsMd5)
408      $allNsMd5[] = $nsMd5;
409    else {
410      $pathDirObj = opendir ($cacheRootDir);
411      while (false !== ($file = readdir ($pathDirObj))) {
412	if (in_array (strtolower ($file), $exclude_array))
413	  continue;
414	$pathFile = $pathDir.$file;
415	if (!is_dir ($pathFile))
416	  continue;
417	$allNsMd5[] = $file;
418      }
419    }
420    foreach ($allNsMd5 as $nsMd5) {
421      $cacheDir = $this->cacheRootDir.$nsMd5."/";
422      if (!is_dir ($cacheDir))
423	break;
424      $pathDirObj = opendir ($cacheDir);
425      while (false !== ($file = readdir ($pathDirObj))) {
426	if (in_array (strtolower ($file), $exclude_array))
427	  continue;
428	$pathFile = $cacheDir.$file;
429	if (!is_file ($pathFile))
430	  continue;
431	if (eregi ('.*\.cache$', $file, $b))
432	  unlink ($pathFile);
433      }
434      @rmdir ($cacheDir);
435    }
436  }
437
438  // ============================================================
439  function manageRecord (&$request, $status, $needCaptcha) {
440    if (!$this->testNotEmpty ())
441      return;
442
443    if ($this->ticketOk ()) {
444      if (!$this->updateRequest ($request, $status))
445	return;
446    } else {
447      $request ['ticket'] = substr (md5 (date ('YmdHis')), 0, 12);
448      $this->md5id = md5 (trim ($request ['ticket']));
449    }
450    if ($needCaptcha && (!$captcha =& plugin_load ('helper', 'captcha') || !$captcha->check ())) {
451      $this->message ('error', $this->getLang ('badCaptcha'));
452      return;
453    }
454    if (!$this->wordOk ()) {
455      $this->message ('error', $this->getLang ('wordMandatory'));
456      return;
457    }
458    if (!$this->translateOk ()) {
459      $this->message ('error', $this->getLang ('translateMandatory'));
460      return;
461
462    }
463    if (! isset ($request['date']) || $request['date'] == "") {
464      $request['date'] = date ('YmdHis');
465      $request['ip'] = $_SERVER ['REMOTE_ADDR'];
466      global $INFO;
467      if (isset ($INFO['userinfo']['mail']))
468	$request['email'] = $INFO['userinfo']['mail'];
469      $request['ns'] = $this->NS;
470    }
471    unset ($request['operation']);
472    sleep (1);
473    if ($status == $this->prop && $this->maxIP ($request))
474      return;
475    $this->writeGlossary ($this->md5id, $request, $status);
476    $this->message ('success', $this->getLang ('proposalRecorded').'<b>'.$request['ticket']."</b>.");
477    $this->adminNotification ($request, $status);
478  }
479
480  // ============================================================
481  // Result for display
482  // ============================================================
483  function getDefinitionSize () {
484    return $this->getGlosarySize ($this->def, $this->md5ns);
485  }
486  function getProposalSize () {
487    return $this->getGlosarySize ($this->prop, $this->md5ns);
488  }
489  function getPollSize () {
490    return $this->getGlosarySize ($this->poll, $this->md5ns);
491  }
492  function printProposal () {
493    global $INFO;
494    $needCaptcha = !(isset ($INFO ['userinfo']) &&
495		     isset ($INFO ['userinfo']['grps']) &&
496		     $INFO ['userinfo']['grps']);
497    $request = &$_REQUEST ['glossary'];
498    if ($request['operation'] == "record")
499      $this->manageRecord ($request, $this->prop, $needCaptcha);
500    echo
501      '<form method="post" action="" onsubmit="return glossaryAjax(this);">'.NL.
502      '  <table>'.NL.
503      '    <tr class="title">'.NL.
504      '      <th><img src="'.$this->imgDir.'stop.png" /> '.$this->getLang ('word').'</th>'.NL.
505      '      <th><img src="'.$this->imgDir.'one-way.png" /> '.$this->getLang ('translate').'</th>'.NL.
506      '    </tr><tr class="odd">'.NL.
507      '      <td>'.NL.
508      '        * <input type="text" name="glossary[word]" value="'.$request['word'].'" class="text" />'.NL.
509      '      </td><td>'.NL.
510      '        * <input type="text" name="glossary[translate]" value="'.$request['translate'].'" class="text" />'.NL.
511      '      </td>'.NL.
512      '    <tr class="title">'.NL.
513      '      <th>'.$this->getLang ('why').'</th>'.NL.
514      '      <th>'.NL.
515      '        <input type="hidden" name="glossary[operation]" value="record">'.NL.
516      '        <input type="hidden" name="glossary[ns]" value="'.$this->NS.'">'.NL.
517      '        <input type="submit" name="glossary[action]" value="'.$this->getLang (empty ($request['ticket']) ? 'proposal' : 'update').'" />'.NL.
518      '        <input type="reset" onClick="glossaryReset(this);glossaryUpdateProposalLabel(this.form)" value="'.$this->getLang ('new').'" />'.NL.
519      '      </th>'.NL.
520      '    </tr><tr class="odd">'.NL.
521      '      <td colspan="2"><textarea name="glossary[why]" class="why" />'.$request['why'].'</textarea></td>'.NL.
522      '    </tr><tr class="even">'.NL.
523      '      <td class="right">'.NL.
524      '        '.$this->getLang ('ticketIfUpdate').NL.
525      '      </td><td>'.NL.
526      '        <input type="text" name="glossary[ticket]" value="'.$request['ticket'].'" class="text" onChange="glossaryUpdateProposalLabel(this.form)" onKeypress="glossaryUpdateProposalLabel(this.form)" />'.NL.
527      '      </td>'.NL.
528      '    </tr>'.NL;
529    if ($needCaptcha && $captcha =& plugin_load ('helper', 'captcha'))
530      echo '<tr><td colspan=2>'.$captcha->getHTML ().'</td></tr>'.NL;
531    echo
532      '  </table>'.NL;
533      '</form>';
534  }
535
536  // ============================================================
537  function printSample () {
538    if (file_exists ($this->sampleFile) &&
539	(time () - filemtime ($this->sampleFile) < $this->getConf ('sampleDelai'))) {
540      echo file_get_contents ($this->sampleFile);
541      return;
542    }
543
544    $all = $this->readAllGlossary ($this->def);
545    $keys = array_keys ($all);
546    $rand = array_rand ($keys);
547    $record = $all [$keys [$rand]];
548    $imgDir = DOKU_REL.'lib/plugins/glossary/images/';
549    foreach (explode ($this->transSep, $record['word']) as $word)
550      foreach (explode ($this->transSep, $record['translate']) as $translate) {
551            $word = trim ($word);
552	    $translate = trim ($translate);
553	    $pageId = trim ($record['useTicket']);
554	    if (!$pageId)
555	      $pageId = trim ($record['ticket']);
556	    resolve_pageid ($this->NS, $pageId, $exists);
557	    $cleanId = cleanId ($pageId);
558	    $link = '<a class="wikilink1" href="'.DOKU_REL.$cleanId.'">';
559	    $text =
560	      '<div>'.$this->getLang ('word').'<br/><img src="'.$imgDir.'stop.png" align="left"/> <span class="glossaryWord"> '.$link.$word.' </a></span></div>'.NL.
561	      '<div>'.$this->getLang ('translate').'<br/><img src="'.$imgDir.'one-way.png" align="left"/> <span class="glossaryTranslate"> '.$link.$translate.' </a></span></div>'.NL;
562	    file_put_contents ($this->sampleFile, $text);
563	    echo $text;
564	    return;
565    }
566  }
567
568  // ============================================================
569  function clearListFile () {
570    @unlink ($this->listFile);
571  }
572
573  function printList () {
574    if ($this->testAdminGroup ()) {
575      echo '<div><form>';
576      foreach (array ('clear', 'clearAll') as $action)
577	echo '<input value="'.$this->getLang ($action).'" onclick="javascript:glossarySendClear (\''.$action.'\', \''.$this->NS.'\')" type="button">';
578      echo '</form></div>';
579    }
580    if (file_exists ($this->listFile) &&
581	(time () - filemtime ($this->listFile) < $this->getConf ('listDelai'))) {
582      echo file_get_contents ($this->listFile);
583      return;
584    }
585
586    $all = $this->readAllGlossary ($this->def);
587    $poll = $this->readAllGlossary ($this->poll);
588    $text =
589      '<table>'.NL.
590      '  <tr class="title">'.NL.
591      '    <th></th>'.NL.
592      '    <th class="toolTip">'.NL.
593      '      <a onClick="glossarySort(this,glossaryComparatorWord);"><span>'.$this->getLang ('tipWord').'</span><img src="'.$this->imgDir.'stop.png" /></a>'.NL.
594      '      <a onClick="glossarySort(this,glossaryComparatorTranslate);"><span>'.$this->getLang ('tipTranslate').'</span><img src="'.$this->imgDir.'one-way.png" /></a>'.NL.
595      '      <a onClick="glossarySort(this,glossaryComparatorDate);"><span>'.$this->getLang ('tipDate').'</span><img src="'.$this->imgDir.'clock.png" /></a>'.NL.
596      '      <a onClick="glossarySort(this,glossaryComparatorView);"><span>'.$this->getLang ('tipView').'</span><img src="'.$this->imgDir.'eye.png" /></a>'.NL.
597      '      <a onClick="glossarySort(this,glossaryComparatorScore);"><span>'.$this->getLang ('tipScore').'</span><img src="'.$this->imgDir.'score-all.png" width="32" /></a>'.NL.
598      '    </th>'.NL.
599      '    <th>'.$this->getLang ('why').'</th>'.NL.
600      '    <th><img src="'.$this->imgDir.'stop.png" /> '.$this->getLang ('word').'</th>'.NL.
601      '    <th colspan="2" class="toolTip" style="white-space: nowrap;">'.NL.
602      '      <form onsubmit="javascript:glossarySearch(this.elements[0]);return false;">'.NL.
603      '        <span>'.$this->getLang ('tipSearch').'</span><img src="'.$this->imgDir.'search.png" />'.NL.
604      '        <input type="text" name="glossary[search]" value="" class="search" keyup="glossarySearch(this);" onChange="glossarySearch(this);" />'.NL.
605      '      </form>'.NL.
606      '    </th>'.NL.
607      '    <th><img src="'.$this->imgDir.'one-way.png" /> '.$this->getLang ('translate').'</th>'.NL.
608      '    <th>'.$this->getLang ('poll').'</th>'.NL.
609      '  </tr>';
610    $even = 0;
611    $recentTime = mktime (0, 0, 0, date ("n"), date ("j")-$this->recentDays, date ("Y"));
612    $recent = date ("YmdHis", $recentTime);
613    global $ID;
614    // XXX bug si empty ($this->transSep)
615    $canInc = true;
616    foreach ($all as $md5id => $record)
617      foreach (explode ($this->transSep, $record['word']) as $word)
618      foreach (explode ($this->transSep, $record['translate']) as $translate) {
619	$word = trim ($word);
620	$translate = trim ($translate);
621	$pageId = trim ($record['useTicket']);
622	if (!$pageId)
623	  $pageId = trim ($record['ticket']);
624	resolve_pageid ($this->NS, $pageId, $exists);
625	$style = false;
626	$imgClock = '';
627	if ($record['date'] > $recent) {
628	  $opacity = 1;
629	  if (preg_match ("#(?<Y>[0-9]{4})(?<m>[0-9]{1,2})(?<d>[0-9]{1,2})#", $record['date'], $dt_date)) {
630	    $delta = (mktime (0, 0, 0, $dt_date["m"], $dt_date["d"],  $dt_date["Y"]) - $recentTime) / 86400;
631	    $opacity = ($delta)/($this->recentDays+1);
632	  }
633	  $imgClock = '<img src="'.$this->imgDir.'clock.png" style="opacity:'.$opacity.';" />';
634	}
635	$cleanId = cleanId ($pageId);
636	if ($canInc && $ID == $cleanId) {
637	  $poll [$md5id] = $this->incView ($md5id, $word);
638	  $canInc = false;
639	}
640	$link = '<a href="'.DOKU_REL.$cleanId.'">';
641	list ($scoreVal, $scoreImg) = $this->getScore ($poll [$md5id]);
642	$text .=
643	  '<tr class="'.$this->nextOddEven ($even).'"'.NL.
644	  '    word="'.$word.'" translate="'.$translate.'"'.NL.
645	  '    date="'.$record['date'].'" view="'.$poll [$md5id]['view'].'" score="'.$scoreVal.'">'.NL.
646	  '    <td class="count"></td>'.NL.
647	  '    <td>'.$imgClock.'</td>'.NL.
648	  '    <td class="why toolTip">'.$link.'<img src="'.$this->imgDir.'help.png"/><span>'.$this->getLang ('tipWhy').'</span></a></td>'.NL.
649	  '    <td class="word" colspan="2">'.$link.$word.'</a></td>'.NL.
650	  '    <td class="translate" colspan="2">'.$link.$translate.'</a></td>'.NL.
651	  '    <td class="poll toolTip">'.$link.'<span>'.$this->getLang ('tipPoll').'</span>'.$scoreImg.'</a></td>'.NL.
652	  '  </tr>';
653      }
654    $text .=
655      NL.
656      '</table>';
657    file_put_contents ($this->listFile, $text);
658    echo $text;
659  }
660
661  // ============================================================
662  function manageUpdate (&$request) {
663    if (!$this->testAdminGroup ())
664      return;
665    $this->manageRecord ($request, $this->def, false);
666  }
667
668  function manageRemove (&$request) {
669    if (!$this->testAdminGroup ())
670      return;
671    foreach (array ($this->prop, $this->def) as $status) {
672      if (empty ($request[$status.'tickets']))
673	continue;
674      foreach ($request[$status.'tickets'] as $ticket) {
675	$this->removeGlossary (md5 (trim ($ticket)), $status);
676	$this->message ('info', $ticket." ".$this->getLang ('ticketDeleted'));
677	if ($status == $this->def) {
678	  $pageId = $ticket;
679	  resolve_pageid ($this->NS, $pageId, $exists);
680	    if ($exists) {
681	      saveWikiText ($pageId, "", "wizard remove");
682	      $this->message ('info', $pageId." ".$this->getLang ('pageDeleted'));
683	    }
684	}
685      }
686    }
687  }
688
689  // ============================================================
690  function printManagedList ($status) {
691    $all = $this->readAllGlossary ($status);
692    echo
693      '<form method="post" action="" onsubmit="return glossaryAjax(this);">'.NL.
694      '  <table>'.NL.
695      '    <tr class="title">'.NL.
696      '      <th></th>'.NL;
697    foreach (array_diff ($this->statusFields [$status], array ("ns")) as $field)
698      echo
699      '      <th>'.$this->getLang ($field).'</th>'.NL;
700    echo
701      '      </tr>';
702    $even = 0;
703    foreach ($all as $record) {
704      echo
705	'<tr class="'.$this->nextOddEven ($even).'">'.NL.
706	'      <td><input type="checkbox" name="glossary['.$status.'tickets][]" value="'.$record['ticket'].'"/></td>'.NL.
707	'      <td>'.substr ($record['date'],0, 8).'</td>'.NL.
708	'      <td>'.$record['ip'].'</td>'.NL.
709	'      <td>'.$record['email'].'</td>'.NL.
710	'      <td>'.$record['ticket'].'</td>'.NL;
711      if ($status == $this->def)
712	echo
713	  '      <td>'.$record['useTicket'].'</td>'.NL;
714      echo
715	'      <td>'.$record['word'].'</td>'.NL.
716	'      <td>'.$record['translate'].'</td>'.NL;
717      if ($status == $this->prop)
718	echo
719	  '      <td>'.str_replace ("\n", "<br/>\n", $record['why']).'</td>'.NL;
720      echo
721	'    </tr>';
722    }
723    echo
724      '<tr>'.NL.
725      '      <td colspan="2">'.NL.
726      '        <input type="hidden" name="glossary[operation]" value="'.$status.'remove">'.NL.
727      '        <input type="hidden" name="glossary[ns]" value="'.$this->NS.'">'.NL.
728      '        <input type="submit" name="glossary[action]" value="'.$this->getLang ('remove').'"/></td>'.NL.
729      '      <td colspan="6"></td>'.NL.
730      '    </tr>'.NL.
731      '  </table>'.NL.
732      ' </form>'.NL;
733  }
734
735  // ============================================================
736  function printManagedForm (&$request) {
737    echo
738      '<form method="post" action="" onsubmit="return glossaryAjax(this);">'.NL.
739      '  <table>'.NL.
740      '    <tr class="title">'.NL.
741      '      <th></th>'.NL.
742      '      <th><img src="'.$this->imgDir.'stop.png" /> '.$this->getLang ('word').'</th>'.NL.
743      '      <th><img src="'.$this->imgDir.'one-way.png" /> '.$this->getLang ('translate').'</th>'.NL.
744      '    </tr><tr class="'.$this->oddEven [0].'">'.NL.
745      '      <td><input type="reset" onClick="glossaryReset(this)" value="'.$this->getLang ('new').'" /></td>'.NL.
746      '      <td><input type="text" name="glossary[word]" value="'.$request['word'].'" class="text" /></td>'.NL.
747      '      <td><input type="text" name="glossary[translate]" value="'.$request['translate'].'" class="text" /></td>'.NL.
748      '    </tr><tr class="title">'.NL.
749      '      <th></th>'.NL.
750      '      <th>'.$this->getLang ('ticket').'</th>'.NL.
751      '      <th>'.$this->getLang ('useTicket').'</th>'.NL.
752      '    </tr><tr class="'.$this->oddEven [0].'">'.NL.
753      '      <td>'.NL.
754      '        <input type="hidden" name="glossary[operation]" value="'.$this->def.'update">'.NL.
755      '        <input type="hidden" name="glossary[ns]" value="'.$this->NS.'">'.NL.
756      '        <input type="submit" name="glossary[action]" value="'.$this->getLang ('update').'"/></td>'.NL.
757      '      <td><input type="text" name="glossary[ticket]" value="'.$request['ticket'].'" class="text" /></td>'.NL.
758      '      <td><input type="text" name="glossary[useTicket]" value="'.$request['useTicket'].'" class="text" /></td>'.NL.
759      '    </tr>'.NL.
760      '  </table>'.NL.
761      '</form>'.NL;
762  }
763
764  // ============================================================
765  function createPage (&$request) {
766    if (!$this->ticketOk ())
767      return;
768    $pageId = trim ($request['useTicket']);
769    if (!$pageId)
770      $pageId = trim ($request['ticket']);
771    $this->md5id = md5 ($pageId);
772    $values = $this->readGlossary ($this->md5id, $this->def, LOCK_SH);
773    if (!$values)
774      return;
775    resolve_pageid ($this->NS, $pageId, $exists);
776    if ($exists)
777      return;
778    $pageTpl = io_readfile ($this->localFN ('pageTemplate'));
779    $wordTpl = io_readfile ($this->localFN ('wordTemplate'));
780    $translateTpl = io_readfile ($this->localFN ('translateTemplate'));
781    $wordList = '';
782    foreach (explode ($this->transSep, $values['word']) as $word)
783      $wordList .= str_replace ("@@WORD@@", $word, $wordTpl);
784    $translateList = '';
785    foreach (explode ($this->transSep, $values['word']) as $word)
786      $translateList .= str_replace ("@@TRANSLATE@@", $word, $translateTpl);
787    $replace = array ('@@WORD@@' => $wordList,
788		      '@@TRANSLATE@@' => $translateList,
789		      '@@POL@@' => $values['ticket'],
790		      '@@PROPOSITIONPAGE@@' => $this->getConf ('propositionPage'));
791    $content = str_replace (array_keys ($replace), array_values ($replace), $pageTpl);
792    saveWikiText ($pageId, $content, "wizard creation");
793    $this->message ('success', $this->getLang ('createPage'));
794  }
795
796  // ============================================================
797  function glossariesRemove (&$request) {
798    if (!$this->testAdminGroup ())
799      return;
800    if (empty ($request['dir']))
801	return;
802    foreach ($request['dir'] as $md5ns) {
803      $subDir = $this->dataRootDir.$md5ns.'/';
804      $ns = $this->readConfig ($subDir);
805      if ($this->getGlosarySize ($this->def, $md5ns)+$this->getGlosarySize ($this->prop, $md5ns) > 0)
806	$this->message ('error', $this->getLang ('glossaryNotEmpty').$ns." (".$md5ns.").");
807      else {
808	if (!is_dir ($subDir))
809	  continue;
810	$pathDirObj = opendir ($subDir);
811	while (false !== ($file = readdir ($pathDirObj))) {
812	  if (!eregi ($this->poll.'(.*)\.xml$', $file, $regs))
813	    continue;
814	  @unlink ($subDir.$file);
815	}
816	@unlink ($subDir.$this->configFile);
817	@rmdir ($subDir);
818	$this->message ('success', $this->getLang ('glossaryRemoved').$ns." (".$md5ns.").");
819      }
820    }
821  }
822
823  function printGlossariesList () {
824    if (!is_dir ($this->dataRootDir))
825      return;
826    $list = array ();
827    $exclude_array = explode ("|", ".|..");
828    $pathDirObj = opendir ($this->dataRootDir);
829    while (false !== ($file = readdir ($pathDirObj))) {
830      $subDir = $this->dataRootDir.$file.'/';
831      if (in_array (strtolower ($file), $exclude_array) || !is_dir ($subDir))
832	continue;
833      $ns = $this->readConfig ($subDir);
834      $list [$file] =
835	array ($ns,
836	       $this->getGlosarySize ($this->def, $file),
837	       $this->getGlosarySize ($this->prop, $file),
838	       $this->getGlosarySize ($this->poll, $file));
839    }
840    $even = 0;
841    echo
842      '<form method="post" action="" onsubmit="return glossaryAjax(this);">'.NL.
843      '  <table>'.NL.
844      '    <tr class="title">'.NL.
845      '      <th></th><th>directory</th><th>nameSpace</th><th>definition</th><th>proposal</th><th>poll</th>'.NL.
846      '    </tr>';
847    foreach ($list as $md5ns => $data) {
848      list ($ns, $def, $prop, $poll) = $data;
849      echo
850	'<tr class="'.$this->nextOddEven ($even).'">'.NL.
851	'      <td><input type="checkbox" name="glossary[dir][]" value="'.$md5ns.'"/></td> '.NL.
852	'      <td>'.$md5ns.'</td>'.NL.
853	'      <td>'.$ns.'</td>'.NL.
854	'      <td>'.$def.'</td>'.NL.
855	'      <td>'.$prop.'</td>'.NL.
856	'      <td>'.$poll.'</td>'.NL.
857	'    </tr>';
858    }
859    echo
860      '<tr class="'.$this->nextOddEven ($even).'">'.NL.
861      '      <td colspan="2">'.NL.
862      '        <input type="hidden" name="glossary[operation]" value="glos-remove">'.NL.
863      '        <input type="submit" name="glossary[action]" value="'.$this->getLang ('remove').'"/></td>'.NL.
864      '      <td colspan="4"></td>'.NL.
865      '    </tr>'.NL.
866      '  </table>'.NL.
867      '</form>';
868  }
869
870  // ============================================================
871  function adminProposal () {
872    if (!$this->testAdminGroup ())
873      return;
874    $request = &$_REQUEST ['glossary'];
875    if ($request['operation'] == $this->prop."remove")
876      $this->manageRemove ($request);
877    $this->printManagedList ($this->prop);
878    $this->resetAdminNotification ();
879  }
880
881  function adminDefinition () {
882    if (!$this->testAdminGroup ())
883      return;
884    $request = &$_REQUEST ['glossary'];
885    if ($request['operation'] == $this->def."remove")
886      $this->manageRemove ($request);
887    if ($request['operation'] == $this->def."update") {
888      $this->manageUpdate ($request);
889      $this->createPage ($request);
890    }
891    $this->clearListFile ();
892    $this->printManagedForm ($request);
893    $this->printManagedList ($this->def);
894  }
895
896  function adminGlossaries () {
897    if (!$this->testAdminGroup ())
898      return;
899    $request = &$_REQUEST ['glossary'];
900    if ($request['operation'] == "glos-remove")
901      $this->glossariesRemove ($request);
902    $this->printGlossariesList ();
903  }
904
905  // ============================================================
906  function resetAdminNotification () {
907    $subDir = $this->dataRootDir.$this->md5ns.'/';
908    $this->readConfig ($subDir);
909    $this->lastNotificationReset = date ('YmdHis');
910    $this->writeConfig ();
911  }
912
913  function adminNotification ($request, $status) {
914    $this->readConfig ($this->dataRootDir.$this->md5ns.'/');
915    if ($this->lastNotification <= $this->lastNotificationReset) {
916      $this->lastNotification = date ('YmdHis');
917
918      global $auth;
919      $users = $auth->retrieveUsers ();
920      foreach ($users as $user => $userinfo) {
921	$mailSubject = $this->getLang ('notifySubject');
922	$mailContent = $this->getLang ('notifyContent');
923	$mailContent = str_replace ('@WORD@', $request['word'], $mailContent);
924	$mailContent = str_replace ('@TRANSLATE@', $request['translate'], $mailContent);
925
926	if (in_array ($this->adminGroup, $userinfo ['grps'])) {
927	  $mailTo      = $userinfo['mail'];
928	  mail_send ($mailTo, $mailSubject, $mailContent);
929	  // XXX $this->debug (print_r (array ($mailTo, $mailSubject, $mailContent), 1));
930	}
931      }
932
933      $this->writeConfig ();
934    }
935  }
936
937  // ============================================================
938}
939