r61000 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r60999‎ | r61000 | r61001 >
Date:06:17, 13 January 2010
Author:ning
Status:deferred (Comments)
Tags:
Comment:
Semantic NotifyMe 0.5
Modified paths:
  • /trunk/extensions/SemanticNotifyMe (added) (history)
  • /trunk/extensions/SemanticNotifyMe/COPYING (added) (history)
  • /trunk/extensions/SemanticNotifyMe/INSTALL (added) (history)
  • /trunk/extensions/SemanticNotifyMe/README (added) (history)
  • /trunk/extensions/SemanticNotifyMe/RELEASE-NOTES (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/SMW_NMDBHelper.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/SMW_NMStorage.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/SMW_NotifyProcessor.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/SNM_Initialize.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/jobs (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/jobs/SMW_NMRefreshJob.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/jobs/SMW_NMSendMailJob.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/storage (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/storage/SMW_NMStorageSQL.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/includes/storage/SMW_SQLStore2_QueriesNM.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/languages (added) (history)
  • /trunk/extensions/SemanticNotifyMe/languages/SMW_NMLanguage.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/languages/SMW_NMLanguageEn.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/Language (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/Language/SMW_NMLanguage.js (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/Language/SMW_NMLanguageEn.js (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/Language/SMW_NMLanguageUserEn.js (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/NotifyMe (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/NotifyMe/NotifyHelper.js (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/NotifyMe/nm_tooltip.js (added) (history)
  • /trunk/extensions/SemanticNotifyMe/scripts/prototype.js (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins/images (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins/images/delete.png (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins/images/minus-act.gif (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins/images/minus-pas.gif (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins/images/plus-act.gif (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins/images/plus-pas.gif (added) (history)
  • /trunk/extensions/SemanticNotifyMe/skins/nm.css (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/SMWNotifyMe.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/SMW_NMSendMailAsync.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/SMW_NotAjaxAccess.php (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/libs (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/libs/NM_yui_autocompletion.js (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/skins (added) (history)
  • /trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/skins/NM_yui_autocompletion.css (added) (history)

Diff [purge]

Index: trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/SMW_NMSendMailAsync.php
@@ -0,0 +1,67 @@
 2+<?php
 3+/*
 4+ * Created on 2009/3/25
 5+ *
 6+ * Author: Dch
 7+ */
 8+
 9+
 10+//get Parameter
 11+$wgRequestTime = microtime(true);
 12+
 13+/** */
 14+# Abort if called from a web server
 15+if ( isset( $_SERVER ) && array_key_exists( 'REQUEST_METHOD', $_SERVER ) ) {
 16+ print "This script must be run from the command line\n";
 17+ exit();
 18+}
 19+
 20+
 21+if( version_compare( PHP_VERSION, '5.0.0' ) < 0 ) {
 22+ print "Sorry! This version of MediaWiki requires PHP 5; you are running " .
 23+ PHP_VERSION . ".\n\n" .
 24+ "If you are sure you already have PHP 5 installed, it may be " .
 25+ "installed\n" .
 26+ "in a different path from PHP 4. Check with your system administrator.\n";
 27+ die( -1 );
 28+}
 29+
 30+// copy from user class
 31+function getUserNMOption( $str ) {
 32+ $options = array();
 33+ $a = explode( "\n", $str );
 34+ foreach ( $a as $s ) {
 35+ $m = array();
 36+ if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
 37+ $options[$m[1]] = $m[2];
 38+ }
 39+ }
 40+ return $options['enotifyme'];
 41+}
 42+
 43+// include commandLine script which provides some basic
 44+// methodes for maintenance scripts
 45+$mediaWikiLocation = dirname(__FILE__) . '/../../../..';
 46+require_once "$mediaWikiLocation/maintenance/commandLine.inc";
 47+
 48+$sStore = smwfGetSemanticStore();
 49+$msgs = $sStore->getUnmailedNMMessages();
 50+foreach($msgs as $msg) {
 51+ // send notifications by mail
 52+ $user_info = $sStore->getUserInfo($msg['user_id']);
 53+ if(($user_info->user_email != '') && getUserNMOption($user_info->user_options)) {
 54+ $name = (($user_info->user_real_name=='')?$user_info->user_name:$user_info->user_real_name);
 55+ $body = "Dear Mr./Mrs. $name,<br/>".$msg['notify'].
 56+ "<br/><br/>Sincerely yours,<br/>SMW NotifyMe Bot";
 57+
 58+ UserMailer::send( //userMailer(
 59+ new MailAddress($user_info->user_email, $name),
 60+ new MailAddress($wgEmergencyContact, 'Admin'),
 61+ 'New SMW Notification comes, from '.$wgSitename,
 62+ $body,
 63+ new MailAddress($wgEmergencyContact, 'Admin'),
 64+ true
 65+ );
 66+ }
 67+}
 68+?>
Index: trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/SMW_NotAjaxAccess.php
@@ -0,0 +1,111 @@
 2+<?php
 3+
 4+global $wgAjaxExportList;
 5+global $smwgNMIP;
 6+
 7+require_once($smwgNMIP . '/includes/SMW_NotifyProcessor.php');
 8+$wgAjaxExportList[] = 'smwf_nm_NotifyAccess';
 9+
 10+
 11+function smwf_nm_NotifyAccess($method, $params) {
 12+ $p_array = explode(",", $params);
 13+ global $smwgQEnabled;
 14+
 15+ $result="Query disabled.";
 16+ if($method == "updateMail"){
 17+ global $wgUser;
 18+ $wgUser->setOption( 'enotifyme', $params );
 19+ $wgUser->saveSettings();
 20+ return 'Update NotifyMe mail setting successfully!';
 21+ }
 22+ else if($method == "addNotify"){
 23+ if ($smwgQEnabled) {
 24+ $result = SMWNotifyProcessor::addNotify(str_replace('&amp;', '&', str_replace('&comma;', ',', $p_array[0])),
 25+ str_replace('&amp;', '&', str_replace('&comma;', ',', $p_array[3])),
 26+ $p_array[1], $p_array[2], implode(",", array_slice($p_array, 4)));
 27+ }
 28+ return $result;
 29+ }
 30+ else if($method == "getQueryResult"){
 31+ if ($smwgQEnabled) {
 32+ $params .= '
 33+| format=table
 34+| link=all';
 35+
 36+ // parse params and answer query
 37+ SMWQueryProcessor::processFunctionParams(SMWNotifyProcessor::getQueryRawParams($params),$querystring,$params,$printouts);
 38+
 39+ $result = SMWQueryProcessor::getResultFromQueryString($querystring,$params,$printouts, SMW_OUTPUT_WIKI);
 40+ switch ($params->format) {
 41+ case 'timeline':
 42+ return $result;
 43+ break;
 44+ case 'eventline':
 45+ return $result;
 46+ break;
 47+ case 'googlepie':
 48+ return $result[0];
 49+ break;
 50+ case 'googlebar':
 51+ return $result[0];
 52+ break;
 53+ case 'exhibit':
 54+ return $result;
 55+ break;
 56+ default:
 57+ }
 58+ global $wgParser;
 59+
 60+ if ( ($wgParser->getTitle() instanceof Title) && ($wgParser->getOptions() instanceof ParserOptions) ) {
 61+ $result = $wgParser->recursiveTagParse($result);
 62+ } else {
 63+ global $wgTitle;
 64+ $popt = new ParserOptions();
 65+ $popt->setEditSection(false);
 66+ $pout = $wgParser->parse($result . '__NOTOC__', $wgTitle, $popt);
 67+ /// NOTE: as of MW 1.14SVN, there is apparently no better way to hide the TOC
 68+ SMWOutputs::requireFromParserOutput($pout);
 69+ $result = $pout->getText();
 70+ }
 71+
 72+ // add target="_new" for all links
 73+ $pattern = "|<a|i";
 74+ $result = preg_replace($pattern, '<a target="_new"', $result);
 75+ }
 76+ return $result;
 77+ }
 78+ else if($method == "updateStates"){
 79+ if ($smwgQEnabled) {
 80+ $result = SMWNotifyProcessor::updateStates($p_array);
 81+ }
 82+ return $result;
 83+ }
 84+ else if($method == "updateReportAll"){
 85+ if ($smwgQEnabled) {
 86+ $result = SMWNotifyProcessor::updateReportAll($p_array);
 87+ }
 88+ return $result;
 89+ }
 90+ else if($method == "updateShowAll"){
 91+ if ($smwgQEnabled) {
 92+ $result = SMWNotifyProcessor::updateShowAll($p_array);
 93+ }
 94+ return $result;
 95+ }
 96+ else if($method == "updateDelegates"){
 97+ if ($smwgQEnabled) {
 98+ $result = SMWNotifyProcessor::updateDelegates(explode("|", $params));
 99+ }
 100+ return $result;
 101+ }
 102+ else if($method == "delNotify"){
 103+ if ($smwgQEnabled) {
 104+ $result = SMWNotifyProcessor::delNotify($p_array);
 105+ }
 106+ return $result;
 107+ }
 108+ else {
 109+ return "Operation failed, please retry later.";
 110+ }
 111+}
 112+?>
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/SMWNotifyMe.php
@@ -0,0 +1,406 @@
 2+<?php
 3+
 4+if (!defined('MEDIAWIKI')) die();
 5+
 6+
 7+
 8+global $IP;
 9+require_once( $IP . "/includes/SpecialPage.php" );
 10+require_once( "SMW_NotAjaxAccess.php" );
 11+
 12+global $smwgNMIP;
 13+
 14+require_once($smwgNMIP . '/includes/SMW_NotifyProcessor.php');
 15+
 16+/*
 17+ * Standard class that is resopnsible for the creation of the Special Page
 18+ */
 19+class SMWNotifyMe extends SpecialPage {
 20+ public function __construct() {
 21+ parent::__construct('NotifyMe');
 22+ }
 23+/*
 24+ * Overloaded function that is responsible for the creation of the Special Page
 25+ */
 26+ public function execute() {
 27+
 28+ global $wgRequest, $wgOut, $smwgNMScriptPath, $wgUser;
 29+
 30+ # Get query parameters
 31+ $feedFormat = $wgRequest->getVal( 'feed' );
 32+ if($feedFormat=='rss') {
 33+ # 10 seconds server-side caching max
 34+ $wgOut->setSquidMaxage( 10 );
 35+
 36+ global $smwgNMMaxFeedItems;
 37+ $limit = ($smwgNMMaxFeedItems > 0) ? $smwgNMMaxFeedItems : 20;
 38+
 39+ $dbr = wfGetDB( DB_SLAVE );
 40+ $id = $wgRequest->getVal( 'uid' );
 41+ $type = "uid";
 42+ if($id) {
 43+ # Get last modified date, for client caching
 44+ # Don't use this if we are using the patrol feature, patrol changes don't update the timestamp
 45+ $lastmod = $dbr->selectField( 'smw_nm_rss', 'MAX(timestamp)', array('user_id'=>$id), 'NotifyMeRSS' );
 46+ } else {
 47+ $type = "nid";
 48+ $id = $wgRequest->getVal( 'nid' );
 49+ $lastmod = $dbr->selectField( 'smw_nm_rss', 'MAX(timestamp)', array('notify_id'=>$id), 'NotifyMeRSS' );
 50+ }
 51+ if( $lastmod && $wgOut->checkLastModified( $lastmod ) ){
 52+ # Client cache fresh and headers sent, nothing more to do.
 53+ return;
 54+ }
 55+ $this->rcOutputFeed( $feedFormat, $type, $id, $limit, $lastmod, true );
 56+ } else {
 57+ $user_id = $wgUser->getId();
 58+
 59+ $wgOut->setPageTitle(wfMsg('smw_notifyme'));
 60+
 61+ if($user_id > 0) {
 62+ $isSysop = in_array('sysop', $wgUser->getEffectiveGroups());
 63+ if($isSysop) {
 64+ SMWNotifyMe::addAutocomplete();
 65+ }
 66+
 67+ $imagepath = $smwgNMScriptPath . '/skins/images/';
 68+
 69+ $html = '<div id="nmcontent">
 70+ <div id="shade" style="display:none"></div>
 71+ <div id="fullpreviewbox" style="display:none">
 72+ <div id="fullpreview"></div>
 73+ <span class="nmbutton" onclick="$(\'fullpreviewbox\', \'shade\').invoke(\'toggle\')"><img src="'. $imagepath. 'delete.png"/>Close Preview</span></div>
 74+ <div id="nmlayout">
 75+ <div id="querytitle" onclick="notifyhelper.switchquery()" onmouseover="Tip(\'' . wfMsg('smw_nm_tt_query') . '\')"><a id="querytitle-link" class="minusplus" href="javascript:void(0)"></a>Notify Me Query</div>
 76+ <div id="querycontent">'.$this->getQueryLayout().'</div>
 77+ <div id="layouttitle" onclick="notifyhelper.switchlayout()" onmouseover="Tip(\'' . wfMsg('smw_nm_tt_nmm') . '\')"><a id="layouttitle-link" class="minusplus" href="javascript:void(0)"></a>Notify Me Manager</div>
 78+ <div id="layoutcontent">' . $this->getNotifyTable().'</div>
 79+ </div>
 80+ </div>';
 81+
 82+ global $wgEmailAuthentication, $wgEnableEmail;
 83+ if ($wgEnableEmail) {
 84+ if ($wgEmailAuthentication && ($wgUser->getEmail() != '') ) {
 85+ if( $wgUser->getEmailAuthenticationTimestamp() ) {
 86+ $disableEmailPrefs = false;
 87+ } else {
 88+ $disableEmailPrefs = true;
 89+ }
 90+ } else {
 91+ $disableEmailPrefs = false;
 92+ }
 93+ $eEmail = $wgUser->getOption( 'enotifyme' );
 94+ $html .= '<div class="nmmenubar">
 95+ <input id="nmemail" type="checkbox" value="1"'.($disableEmailPrefs?' disabled':'').($eEmail?' checked':'').'/> Enable \'Notify Me\' by E-mail ';
 96+ if($disableEmailPrefs) {
 97+ $href = htmlspecialchars( $wgUser->getSkin()->makeSpecialUrl( 'Preferences' ) );
 98+ $text = htmlspecialchars( wfMsg( 'mypreferences' ) );
 99+ $html .= " (Please enable your email account in '<a href=\"$href\">$text</a>'. )";
 100+ }
 101+ $html .= '</div>';
 102+ }
 103+
 104+ $html .= '<div>
 105+ RSS Feed : <input id="nmrss" size="80" type="text" title="RSS feed url" value="'.$this->getTitle()->getFullURL( 'feed=rss&uid='.$user_id ).'" />&nbsp;&nbsp;
 106+ <button class="btn" onclick="notifyhelper.copyToClipboard(\'nmrss\')" onmouseover="this.className=\'btn btnhov\'; Tip(\'' . wfMsg('smw_nm_tt_clipboard') . '\')" onmouseout="this.className=\'btn\'">' . wfMsg('smw_qi_clipboard') . '</button>
 107+ </div>';
 108+
 109+ $html .= '<script type="text/javascript" src="' . $smwgNMScriptPath . '/scripts/NotifyMe/nm_tooltip.js"></script>';
 110+ } else {
 111+ $html = '<div id="nmlayout">You have not logged in, please login first. Thanks.</div>';
 112+ }
 113+ $wgOut->addHTML($html);
 114+ }
 115+ }
 116+ private function getQueryLayout() {
 117+ global $wgUser;
 118+ $isSysop = in_array('sysop', $wgUser->getEffectiveGroups());
 119+
 120+ $html = '<table style="width: 100%;"><tr><td width=40%>
 121+ <table width=100%>
 122+ <tr><th width=40%>Name :</th><td width=60% onmouseover="Tip(\'Name of the notification\')"><input type="text" id="nmqname"></td></tr>
 123+ <tr><th nowrap>Report all :</th><td onmouseover="Tip(\'Report all semantic semantic attribes\\\' change of monitored pages\')"><input type="checkbox" checked id="nmqrall"></td></tr>
 124+ <tr><th nowrap>Show all :</th><td onmouseover="Tip(\'Show all query results with notifications\')"><input type="checkbox" id="nmqsall"></td></tr>';
 125+ if($isSysop) {
 126+ $html .= '<tr><th nowrap>Delegate to :</th><td><input type="text" id="nmd_new" size=35>
 127+ <div class="page_name_auto_complete" id="nmdiv_new"></div></td>';
 128+ }
 129+ $html .= '</table></td><td width=60%><table style="width: 100%;">
 130+ <tr><th onmouseover="Tip(\'' . wfMsg('smw_nm_tt_qtext') . '\')">Query (table format, all link only)</th></tr>
 131+ <tr><td onmouseover="Tip(\'Full query string, with {{#ask syntax\')">
 132+ {{#ask:<br/>
 133+ <textarea id="nmquery" cols="20" rows="6"></textarea><br/>
 134+ | format=table<br/>
 135+ | link=all<br/>
 136+ |}}
 137+ </td></tr>
 138+ </table>
 139+ </td></tr></table>
 140+ <div class="nmmenubar">
 141+ <div style="text-align:left;float:left;">
 142+ <button class="btn" onclick="notifyhelper.previewQuery()" onmouseover="this.className=\'btn btnhov\'; Tip(\'Show full preview of your query results\')" onmouseout="this.className=\'btn\'">Preview Results</button>
 143+ <button class="btn" onclick="notifyhelper.doSaveToNotify('.$isSysop.')" onmouseover="this.className=\'btn btnhov\'; Tip(\'Add this notification to you NotifyMe\')" onmouseout="this.className=\'btn\'">Add to NotifyMe</button>
 144+ </div><div style="text-align:right;">
 145+ <button class="btn" onclick="notifyhelper.resetQuery()"onmouseout="this.className=\'btn\'" onmouseover="this.className=\'btn btnhov\'; Tip(\'Resets the entire query\')">Reset Query</button>
 146+ </div></div>';
 147+
 148+ return $html;
 149+ }
 150+
 151+ static function addAutocomplete() {
 152+ global $smwgNMDelegateQuery, $smwgNMDelegateUserGroup;
 153+ if(!$smwgNMDelegateQuery && !$smwgNMDelegateUserGroup) return;
 154+
 155+ global $wgOut, $smwgNMScriptPath;
 156+ $nmScriptPath = $smwgNMScriptPath . '/specials/SMWNotifyMe';
 157+ $nmYUIBase = "http://yui.yahooapis.com/2.7.0/build/";
 158+
 159+ $wgOut->addLink( array(
 160+ 'rel' => 'stylesheet',
 161+ 'type' => 'text/css',
 162+ 'media' => "screen, projection",
 163+ 'href' => $nmScriptPath . '/skins/NM_yui_autocompletion.css'
 164+ ));
 165+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'yahoo/yahoo-min.js"></script>' . "\n");
 166+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'dom/dom-min.js"></script>' . "\n");
 167+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'event/event-min.js"></script>' . "\n");
 168+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'get/get-min.js"></script>' . "\n");
 169+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'connection/connection-min.js"></script>' . "\n");
 170+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'json/json-min.js"></script>' . "\n");
 171+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'datasource/datasource-min.js"></script>' . "\n");
 172+ $wgOut->addScript('<script type="text/javascript" src="' . $nmYUIBase . 'autocomplete/autocomplete-min.js"></script>' . "\n");
 173+ $wgOut->addScript('<script type="text/javascript" src="' . $nmScriptPath . '/libs/NM_yui_autocompletion.js"></script>' . "\n");
 174+
 175+ $pages = array();
 176+ if($smwgNMDelegateUserGroup) {
 177+ if($smwgNMDelegateUserGroup != "*" && !is_array($smwgNMDelegateUserGroup)) {
 178+ $groups = split(",", $smwgNMDelegateUserGroup);
 179+ for($i = count($groups)-1; $i>=0; --$i) {
 180+ $groups[$i] = strtolower(trim(str_replace("\'", "\\\'", $groups[$i])));
 181+ }
 182+ }
 183+ $pages = NMStorage::getDatabase()->getGroupedUsers($groups);
 184+ for($i = count($pages)-1; $i>=0; --$i) {
 185+ $pages[$i] = "['".str_replace("\'", "\\\'", $pages[$i])."']";
 186+ }
 187+ }
 188+ if($smwgNMDelegateQuery) {
 189+ global $smwgQDefaultNamespaces, $smwgQFeatures;
 190+ $qp = new SMWQueryParser($smwgQFeatures);
 191+ $qp->setDefaultNamespaces($smwgQDefaultNamespaces);
 192+ $desc = $qp->getQueryDescription($smwgNMDelegateQuery);
 193+
 194+ $desc->prependPrintRequest(new SMWPrintRequest(SMWPrintRequest::PRINT_THIS, ""));
 195+
 196+ $query = new SMWQuery($desc, true);
 197+
 198+ $query_result = smwfGetStore()->getQueryResult($query);
 199+ while ($res = $query_result->getNext()) {
 200+ $pages[] = "['".str_replace("\'", "\\\'", $res[0]->getNextObject()->getWikiValue())."']";
 201+ }
 202+ }
 203+ global $smwgNMMaxAutocompleteValues;
 204+ if($smwgNMMaxAutocompleteValues <= 0) {
 205+ $smwgNMMaxAutocompleteValues = 10;
 206+ }
 207+ $wgOut->addScript('<script type="text/javascript">
 208+ nmautocompletestrings = ['.join(',', $pages).'];
 209+ nmMaxResultsDisplayed = '.$smwgNMMaxAutocompleteValues.';
 210+ </script>');
 211+ }
 212+ private function getNotifyTable() {
 213+ global $wgUser;
 214+ $isSysop = in_array('sysop', $wgUser->getEffectiveGroups());
 215+
 216+ $cols = 6;
 217+ $html = '<table width="100%" class="smwtable" id="nmtable">
 218+ <tr><th width="5%" onmouseover="Tip(\'Select notifications to be deleted\')">Delete?</th>
 219+ <th width="20%" onmouseover="Tip(\'Name of the notification\')">Name</th>
 220+ <th width="40%" onmouseover="Tip(\'Full query string, with {{#ask syntax\')" nowrap>Query String</th>
 221+ <th width="5%" onmouseover="Tip(\'Report all semantic semantic attribes\\\' change of monitored pages\')" nowrap>Report All</th>
 222+ <th width="5%" onmouseover="Tip(\'Show all query results with notifications\')" nowrap>Show All</th>
 223+ <th width="5%" onmouseover="Tip(\'Current state of notification. Enabled?\')">Enabled?</th>';
 224+ if($isSysop) {
 225+ $cols ++;
 226+ $html .= '<th width="20%" onmouseover="Tip(\'Add delegate users to recieve NotifyMe, separated by comma\')">Delegate</th>';
 227+ }
 228+ $html .= '</tr>';
 229+ $notifications = SMWNotifyProcessor::getNotifications();
 230+ if ($notifications != null) {
 231+ foreach ($notifications as $row) {
 232+ $html .= '<tr>
 233+ <td><input type="checkbox" name="nmdel" value='.$row['notify_id'].'></td>
 234+ <td><a target="_blank" href="'.$this->getTitle()->getFullURL( 'feed=rss&nid='.$row['notify_id'] ).'">'.$row['name'].'</a></td>
 235+ <td>'.str_replace("\n", "<br/>", $row['query']).'</td>
 236+ <td><input type="checkbox" '.($row['rep_all']?'checked':'').' name="nmall" value='.$row['notify_id'].'></td>
 237+ <td><input type="checkbox" '.($row['show_all']?'checked':'').' name="nmsall" value='.$row['notify_id'].'></td>
 238+ <td><input type="checkbox" '.($row['enable']?'checked':'').' name="nmenable" id="nmenable_'.$row['notify_id'].'" value='.$row['notify_id'].'></td>';
 239+ if($isSysop) {
 240+ $html .= '<td><input type="text" name="nmdelegate" id=nmd_'.$row['notify_id'].' value="'.$row['delegate'].'">
 241+ <div class="page_name_auto_complete" id="nmdiv_'.$row['notify_id'].'"></div></td>';
 242+ }
 243+ $html .= '</tr>';
 244+ }
 245+ }
 246+ $html .= '<tr id="nmtoolbar">
 247+ <td><a href="#" onclick="notifyhelper.delall(true)">ALL</a>/<a href="#" onclick="notifyhelper.delall(false)">NONE</a>&nbsp; <button class="btn" onclick="notifyhelper.deleteNotify()" onmouseover="this.className=\'btn btnhov\'; Tip(\'Delete the checked notifications\')" onmouseout="this.className=\'btn\'">Update</button></td>
 248+ <td></td><td></td>
 249+ <td><a href="#" onclick="notifyhelper.reportall(true)">ALL</a>/<a href="#" onclick="notifyhelper.reportall(false)">NONE</a>&nbsp; <button class="btn" onclick="notifyhelper.updateReportAll()" onmouseover="this.className=\'btn btnhov\'; Tip(\'Report all semantic attributes\\\' change on checked notifications\')" onmouseout="this.className=\'btn\'">Update</button></td>
 250+ <td><a href="#" onclick="notifyhelper.showall(true)">ALL</a>/<a href="#" onclick="notifyhelper.showall(false)">NONE</a>&nbsp; <button class="btn" onclick="notifyhelper.updateShowAll()" onmouseover="this.className=\'btn btnhov\'; Tip(\'Show all query results with notifications\')" onmouseout="this.className=\'btn\'">Update</button></td>
 251+ <td><a href="#" onclick="notifyhelper.enableall(true)">ALL</a>/<a href="#" onclick="notifyhelper.enableall(false)">NONE</a>&nbsp; <button class="btn" onclick="notifyhelper.updateStates()" onmouseover="this.className=\'btn btnhov\'; Tip(\'Update the states of your notifications, enable or disable them\')" onmouseout="this.className=\'btn\'">Update</button></td>';
 252+ if($isSysop)
 253+ $html .= '<td><button class="btn" onclick="notifyhelper.updateDelegate()" onmouseover="this.className=\'btn btnhov\'; Tip(\'Update delegate\')" onmouseout="this.className=\'btn\'">Update</button></td>';
 254+ $html .= '</tr></table>';
 255+ return $html;
 256+ }
 257+
 258+ // rss feed related features
 259+ function rcOutputFeed( $feedFormat, $type, $id, $limit, $lastmod, $queryresult_item = false) {
 260+ global $messageMemc, $wgFeedCacheTimeout;
 261+ global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode;
 262+
 263+ if( !isset( $wgFeedClasses[$feedFormat] ) ) {
 264+ wfHttpError( 500, "Internal Server Error", "Unsupported feed type." );
 265+ return false;
 266+ }
 267+
 268+ $timekey = wfMemcKey( 'nmfeed', $feedFormat, $type, $id, 'timestamp' );
 269+ $key = wfMemcKey( 'nmfeed', $feedFormat, $type, $id, 'limit', $limit );
 270+
 271+ //purge cache if requested
 272+ global $wgRequest, $wgUser;
 273+ $purge = $wgRequest->getVal( 'action' ) == 'purge';
 274+ if ( $purge && $wgUser->isAllowed('purge') ) {
 275+ $messageMemc->delete( $timekey );
 276+ $messageMemc->delete( $key );
 277+ }
 278+
 279+ /**
 280+ * Bumping around loading up diffs can be pretty slow, so where
 281+ * possible we want to cache the feed output so the next visitor
 282+ * gets it quick too.
 283+ */
 284+ $cachedFeed = false;
 285+ if( ( $wgFeedCacheTimeout > 0 ) && ( $feedLastmod = $messageMemc->get( $timekey ) ) ) {
 286+ /**
 287+ * If the cached feed was rendered very recently, we may
 288+ * go ahead and use it even if there have been edits made
 289+ * since it was rendered. This keeps a swarm of requests
 290+ * from being too bad on a super-frequently edited wiki.
 291+ */
 292+ if( time() - wfTimestamp( TS_UNIX, $feedLastmod ) < $wgFeedCacheTimeout ||
 293+ wfTimestamp( TS_UNIX, $feedLastmod ) > wfTimestamp( TS_UNIX, $lastmod ) ) {
 294+ wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
 295+ $cachedFeed = $messageMemc->get( $key );
 296+ } else {
 297+ wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
 298+ }
 299+ }
 300+
 301+ $feedTitle = $wgSitename . ' - Recent changes to ';
 302+ if($type = "nid") {
 303+ $dbr = wfGetDB( DB_SLAVE );
 304+ $notify_name = $dbr->selectField( 'smw_nm_query', 'name', array('notify_id'=>$id), 'NotifyMeRSS' );
 305+ $feedTitle .= "\"$notify_name\"";
 306+ } else {
 307+ $feedTitle .= "Notify Me";
 308+ }
 309+ $feedTitle .= ' [' . $wgContLanguageCode . ']';
 310+
 311+ $feed = new $wgFeedClasses[$feedFormat](
 312+ $feedTitle,
 313+ 'Track the most recent changes to '.($notify_name?"\"$notify_name\"":'Notify Me').' in this feed.',
 314+ $wgTitle->getFullUrl() );
 315+
 316+ if( is_string( $cachedFeed ) ) {
 317+ wfDebug( "RC: Outputting cached feed\n" );
 318+ $feed->httpHeaders();
 319+ echo $cachedFeed;
 320+ } else {
 321+ wfDebug( "RC: rendering new feed and caching it\n" );
 322+ ob_start();
 323+ $this->rcDoOutputFeed( $feed, $queryresult_item, $type, $id, $limit );
 324+ $cachedFeed = ob_get_contents();
 325+ ob_end_flush();
 326+
 327+ $expire = 3600 * 24; # One day
 328+ $messageMemc->set( $key, $cachedFeed );
 329+ $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
 330+ }
 331+ return true;
 332+ }
 333+
 334+ function rcDoOutputFeed( &$feed, $queryresult_item, $type, $id, $limit ) {
 335+ wfProfileIn( __METHOD__ );
 336+
 337+ $feed->outHeader();
 338+
 339+ if($queryresult_item && $type=="nid") {
 340+ $dbr = wfGetDB( DB_SLAVE );
 341+ $showall = $dbr->selectField( 'smw_nm_query', 'show_all', array('notify_id'=>$id), 'NotifyMeRSS' );
 342+ if($showall) {
 343+ $query = $dbr->selectField( 'smw_nm_query', 'query', array('notify_id'=>$id), 'NotifyMeRSS' );
 344+ SMWQueryProcessor::processFunctionParams(explode("\n", $query), $querystring, $params, $printouts);
 345+ $query = SMWQueryProcessor::createQuery($querystring, $params, SMWQueryProcessor::INLINE_QUERY, 'auto', $printouts);
 346+ $res = smwfGetStore()->getQueryResult($query);
 347+
 348+ $items = array();
 349+ $labels = array();
 350+ foreach ($res->getPrintRequests() as $pr) {
 351+ $labels[] = $pr->getText(SMW_OUTPUT_WIKI);
 352+ }
 353+ $row = $res->getNext();
 354+ $linker = new Linker();
 355+ while ( $row !== false ) {
 356+ $wikipage = $row[0]->getNextObject(); // get the object
 357+ $a = new Article($wikipage->getTitle());
 358+ $description = "<table style=\"width: 60em; font-size: 90%; border: 1px solid #aaaaaa; background-color: #f9f9f9; color: black; margin-bottom: 0.5em; margin-left: 1em; padding: 0.2em; clear: right; text-align:left;\"><tr><th style=\"text-align: center; background-color:#ccccff;\" colspan=\"2\"><big>".$wikipage->getText()."</big></th></tr>";
 359+ $idx = 0;
 360+ foreach ($row as $field) {
 361+ $description .= "<tr><td>".$labels[$idx]."</td><td>";
 362+ $first_value = true;
 363+ while ( ($object = $field->getNextObject()) !== false ) {
 364+ if ($first_value) $first_value = false; else $description .= ', ';
 365+ $description .= $object->getShortText(SMW_OUTPUT_HTML, $linker);
 366+ }
 367+ $description .= "</td></tr>";
 368+ $idx ++;
 369+ }
 370+ $description .= "</table>";
 371+ $items[] = array ('title'=>$wikipage->getText(),'notify'=>$description,'timestamp'=>$a->getTimestamp());
 372+ $row = $res->getNext();
 373+ }
 374+ } else {
 375+ $items = NMStorage::getDatabase()->getNotifyRSS($type, $id, $limit);
 376+ }
 377+ } else {
 378+ $items = NMStorage::getDatabase()->getNotifyRSS($type, $id, $limit);
 379+ }
 380+ foreach( $items as $i ) {
 381+ if(isset($i['link']) && $i['link']) {
 382+ $item = new FeedItem(
 383+ $i['title'],
 384+ $i['notify'],
 385+ $i['link'],
 386+ $i['timestamp']
 387+ );
 388+ } else {
 389+ $title = Title::makeTitle( NS_MAIN, $i['title'] );
 390+ $talkpage = $title->getTalkPage();
 391+ $item = new FeedItem(
 392+ $title->getPrefixedText(),
 393+ $i['notify'],
 394+ $title->getFullURL(),
 395+ $i['timestamp'],
 396+ "",
 397+ $talkpage->getFullURL()
 398+ );
 399+ }
 400+ $feed->outItem( $item );
 401+ }
 402+
 403+ $feed->outFooter();
 404+ wfProfileOut( __METHOD__ );
 405+ }
 406+}
 407+?>
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/skins/NM_yui_autocompletion.css
@@ -0,0 +1,51 @@
 2+/* Custom styles for Semantic Forms autocompletion with Yahoo! UI (YUI) */
 3+/* Most of these calls are meant to get the look to match YUI's "Sam"
 4+ skin, for those wikis that don't have a "yui-skin-sam" declaration
 5+ anywhere in their skin. */
 6+
 7+.yui-ac-content ul {
 8+ margin: 0pt;
 9+ padding: 0pt;
 10+ width: 100%;
 11+}
 12+.yui-ac-content li {
 13+ cursor: default;
 14+ margin: 0pt;
 15+ padding: 2px 40px 2px 5px;
 16+ white-space: nowrap;
 17+}
 18+.yui-skin-sam .yui-ac-content li.yui-ac-highlight {
 19+ color: white;
 20+ background-color: #222288;
 21+}
 22+.yui-ac-content li.yui-ac-highlight {
 23+ color: white;
 24+ background-color: #222288;
 25+}
 26+.yui-ac-content {
 27+ background: #FFFFFF none repeat scroll 0%;
 28+ border: 1px solid #808080;
 29+ overflow:hidden;
 30+ position: absolute;
 31+ z-index: 9050;
 32+}
 33+.yui-ac-bd ul {
 34+ margin-left: 0;
 35+ list-style-image: none;
 36+ list-style-type: none;
 37+}
 38+/* Override unneeded settings from YUI's Sam skin */
 39+.yui-skin-sam .yui-ac-content {
 40+ width: auto;
 41+}
 42+.yui-skin-sam .yui-ac-content li {
 43+ padding-right: 40px;
 44+}
 45+.yui-skin-sam .yui-ac-input {
 46+ width: auto;
 47+ position: static;
 48+}
 49+.yui-skin-sam .yui-ac-container {
 50+ width: auto;
 51+ position: static;
 52+}
Index: trunk/extensions/SemanticNotifyMe/specials/SMWNotifyMe/libs/NM_yui_autocompletion.js
@@ -0,0 +1,152 @@
 2+/*
 3+ * nm_autocomplete()
 4+ *
 5+ * Creates an autocompletion widget, using the Yahoo! UI (YUI) library,
 6+ * for the specified form input, using the specified values and delimiter
 7+ * (in the case that it's a multiple-values autocompletion)
 8+ */
 9+
 10+function nm_autocomplete(input_name, container_name, values, delimiter) {
 11+ if (values != null) {
 12+ this.oACDS = new YAHOO.widget.DS_JSFunction(nmAutocompleteFunctionGenerator(values));
 13+ this.oACDS.maxCacheEntries = 0;
 14+ this.oAutoComp = new YAHOO.widget.AutoComplete(input_name, container_name, this.oACDS);
 15+ }
 16+
 17+ // Instantiate AutoComplete
 18+ this.oAutoComp.alwaysShowContainer = false;
 19+ //this.oAutoComp.minQueryLength = 1;
 20+ this.oAutoComp.maxResultsDisplayed = nmMaxResultsDisplayed;
 21+ this.oAutoComp.animHoriz = false;
 22+ this.oAutoComp.animVert = false;
 23+ if (delimiter != '') {
 24+ this.oAutoComp.delimChar = delimiter;
 25+ }
 26+ this.oAutoComp.minQueryLength = 0;
 27+ this.oAutoComp.textboxFocusEvent.subscribe(
 28+ function (sType, aArgs) {
 29+ var oAutoComp = aArgs[0];
 30+ oAutoComp.sendQuery("");
 31+ });
 32+
 33+ // don't set IFrame, which is meant to improve formatting on Internet
 34+ // Explorer - currently, it only messes up the formatting
 35+ //this.oAutoComp.useIFrame = true;
 36+ // This function returns markup that bolds the original query
 37+ this.oAutoComp.formatResult = function(aResultItem, sQuery) {
 38+ var sKey = aResultItem[0]; // the entire result key
 39+ var sKeyIndex = sKey.toLowerCase().indexOf(sQuery.toLowerCase());
 40+ // if it's not the very beginning, make sure the query string
 41+ // comes after a space
 42+ if (sKeyIndex > 0) {
 43+ sKeyIndex = sKey.toLowerCase().indexOf(' ' + sQuery.toLowerCase()) + 1;
 44+ }
 45+ var sKeyBefore = sKey;
 46+ var sKeyQuery = "";
 47+ var sKeyAfter = "";
 48+ if (sKeyIndex >= 0) {
 49+ sKeyBefore = sKey.substr(0, sKeyIndex);
 50+ sKeyQuery = sKey.substr(sKeyIndex, sQuery.length); // the query itself
 51+ sKeyAfter = sKey.substr(sKeyIndex + sQuery.length); // the rest of the result
 52+ }
 53+
 54+ var aMarkup = sKeyBefore +
 55+ "<span style='font-weight:bold'>" +
 56+ sKeyQuery +
 57+ "</span>" +
 58+ sKeyAfter;
 59+ return aMarkup;
 60+ };
 61+
 62+ // Show custom message if no results found
 63+ this.myOnDataReturn = function(sType, aArgs) {
 64+ var oAutoComp = aArgs[0];
 65+ var sQuery = aArgs[1];
 66+ var aResults = aArgs[2];
 67+
 68+ if(aResults.length == 0) {
 69+ oAutoComp.setBody("<div>No matching results</div>");
 70+ }
 71+ };
 72+
 73+ var itemSelectHandler = function (oAutoComp, listItem, dataArray) {
 74+ }
 75+
 76+ // neither of these events are necessary to subscribe to yet
 77+ //this.oAutoComp.itemSelectEvent.subscribe(itemSelectHandler);
 78+ //this.oAutoComp.dataReturnEvent.subscribe(this.myOnDataReturn);
 79+
 80+ // Preload content in the container
 81+ //this.oAutoComp.sendQuery("");
 82+};
 83+
 84+/*
 85+ * Helper function - returns a function that returns, for a given query
 86+ * string, an array of values that match it
 87+ */
 88+function nmAutocompleteFunctionGenerator(values_list) {
 89+ return function (sQuery) {
 90+ var primaryResults = [];
 91+ var secondaryResults = [];
 92+ if (sQuery && sQuery.length > 0) {
 93+ // in some cases, decodeURI() doesn't handle colons correctly -
 94+ // replace them manually
 95+ query_str = decodeURI(sQuery).replace(/(%3A)/g, ":").toLowerCase();
 96+ for (var i = 0; i < values_list.length; i++) {
 97+ subarray = values_list[i];
 98+ // workaround for strange IE bug
 99+ var name;
 100+ if (subarray.length > 1) {
 101+ name = subarray;
 102+ } else {
 103+ name = subarray[0];
 104+ }
 105+ name_str = name.toLowerCase();
 106+ var sKeyIndex = name_str.indexOf(query_str);
 107+ if (sKeyIndex == 0) {
 108+ primaryResults.push(subarray);
 109+ } else {
 110+ var index2 = name_str.indexOf(" " + query_str);
 111+ if (index2 >= 0) {
 112+ secondaryResults.push(subarray);
 113+ }
 114+ }
 115+ }
 116+ var aResults = primaryResults.concat(secondaryResults);
 117+ // return no values if there's only one value, and the query
 118+ // exactly matches it
 119+ if (aResults.length == 1 && query_str.length == aResults[0][0].length) {
 120+ return [];
 121+ } else {
 122+ return aResults;
 123+ }
 124+ }
 125+ // Empty queries return all values
 126+ else {
 127+ return values_list;
 128+ }
 129+ }
 130+}
 131+
 132+function nmAttachAutocompleteToAllFields()
 133+{
 134+ var inputs = document.getElementsByName("nmdelegate");
 135+ var y;
 136+ for (y = 0; y < inputs.length; y++) {
 137+ nmAttachAutocompleteToField(inputs[y].id);
 138+ }
 139+ nmAttachAutocompleteToField("nmd_new");
 140+}
 141+
 142+var nmautocompletestrings = new Array();
 143+var nmMaxResultsDisplayed = 10;
 144+
 145+//Activate autocomplete functionality for the specified field
 146+function nmAttachAutocompleteToField(input_id)
 147+{
 148+ var div_id = input_id.replace(/nmd_/g, 'nmdiv_');
 149+ var delimiter = ",";
 150+ nm_autocomplete(input_id, div_id, nmautocompletestrings, delimiter);
 151+}
 152+
 153+YAHOO.util.Event.addListener(window, 'load', nmAttachAutocompleteToAllFields);
Index: trunk/extensions/SemanticNotifyMe/INSTALL
@@ -0,0 +1,18 @@
 2+== Requirements ==
 3+
 4+* MediaWiki 1.13.5
 5+* Semantic MediaWiki 1.4.2
 6+* PHP 5.x or greater installed and working
 7+* MySQL >= 4.0.14 (version required by MediaWiki)
 8+* Halo extension 1.4.4 (optional)
 9+
 10+== Installation ==
 11+
 12+Edit $IP/LocalSettings.php and add:
 13+
 14+ include_once('extensions/SemanticNotifyMe/includes/SNM_Initialize.php');
 15+
 16+Apply this patch, $IP/extensions/SemanticMediaWiki/includes/storage/SMW_SQLStore2.php
 17+line 1268, add the following
 18+ $title = Title::makeTitle(NS_SPECIAL, 'SMWNotifyMe');
 19+ $updatejobs[] = new SMWNMRefreshJob($title);
Index: trunk/extensions/SemanticNotifyMe/RELEASE-NOTES
@@ -0,0 +1,9 @@
 2+For a documentation of all features, see http://www.mediawiki.org/wiki/Extension:Semantic_NotifyMe
 3+
 4+== Semantic NotifyMe 0.5 ==
 5+
 6+This is a pre-alpha version of the Semantic NotifyMe extensions.
 7+It includes:
 8+
 9+* support for realtime notification on Semantic property/value changing
 10+* Special:NotifyMe, manage all NotifyMe notifications, delegation enabled
Index: trunk/extensions/SemanticNotifyMe/includes/SMW_NotifyProcessor.php
@@ -0,0 +1,1655 @@
 2+<?php
 3+/**
 4+ * This file contains a static class for accessing functions to generate and execute
 5+ * notify me semantic queries and to serialise their results.
 6+ *
 7+ * @author dch
 8+ */
 9+
 10+global $smwgIP, $smwgNMIP ;
 11+require_once($smwgIP . '/includes/storage/SMW_Store.php');
 12+require_once( $smwgNMIP . '/includes/SMW_NMStorage.php' );
 13+
 14+/**
 15+ * Static class for accessing functions to generate and execute semantic queries
 16+ * and to serialise their results.
 17+ */
 18+class SMWNotifyProcessor {
 19+
 20+ static public function getNotifications() {
 21+ $sStore = NMStorage::getDatabase();
 22+ global $wgUser;
 23+ $user_id = $wgUser->getId();
 24+
 25+ $notifications = $sStore->getNotifications($user_id);
 26+
 27+ return $notifications;
 28+ }
 29+
 30+ /**
 31+ * Enable a NotifyMe with specified id and querystring
 32+ *
 33+ * used for inline query only
 34+ */
 35+ static public function enableNotify($notify_id, $querystring, &$msg = NULL) {
 36+ wfProfileIn('SMWNotifyProcessor::enableNotify (SMW)');
 37+
 38+ $sStore = NMStorage::getDatabase();
 39+ global $smwgQDefaultNamespaces;
 40+
 41+ SMWQueryProcessor::processFunctionParams(SMWNotifyProcessor::getQueryRawParams($querystring),$querystring,$params,$printouts);
 42+ $relatedArticles = array();
 43+ foreach($printouts as $po) {
 44+ if($po == $params['sort']) $sorted = true;
 45+ $printoutArticles[] = array(
 46+ 'namespace' => SMW_NS_PROPERTY,
 47+ 'title' => Title::makeTitle( SMW_NS_PROPERTY, $po->getText() )->getDBkey());
 48+ }
 49+ if(!$sorted && isset($params['sort'])) {
 50+ $printoutArticles[] = array(
 51+ 'namespace' => SMW_NS_PROPERTY,
 52+ 'title' => Title::makeTitle( SMW_NS_PROPERTY, $params['sort'] )->getDBkey());
 53+ }
 54+
 55+ $qp = new SMWNotifyParser($notify_id, $printoutArticles);
 56+ $qp->setDefaultNamespaces($smwgQDefaultNamespaces);
 57+ $desc = $qp->getQueryDescription($querystring);
 58+
 59+ if(!$qp->m_result) {
 60+ $qp->m_errors[] = "The category / instance / property page in query may not exists.";
 61+ }
 62+
 63+ if(isset($msg) && $qp->hasSubquery()) {
 64+ $msg .= "\nThe query contains subquery, which may affect the precision of notifications.";
 65+ }
 66+
 67+ $query = new SMWQuery($desc, true, false);
 68+ $query->setQueryString($querystring);
 69+ $query->addErrors($qp->getErrors()); // keep parsing errors for later output
 70+
 71+ $res = $sStore->getNMQueryResult($query);
 72+
 73+ if(count($query->getErrors())>0) {
 74+ if(isset($msg)) {
 75+ $msg .= "\n\n" . implode('\n', $query->getErrors()) . "\n\nYou can enable the query in NotifyMe manager later.";
 76+ }
 77+ $sStore->disableNotifyState($notify_id);
 78+ wfProfileOut('SMWNotifyProcessor::enableNotify (SMW)');
 79+ return false;
 80+ }
 81+
 82+ $sStore->updateNMSql($notify_id, $res['sql'], $res['tmp_hierarchy']);
 83+ if (count($res['page_ids']) > 0) {
 84+ $add_monitor = array();
 85+ foreach($res['page_ids'] as $page_id) {
 86+ $add_monitor[] = array('notify_id'=>$notify_id, 'page_id'=>$page_id);
 87+ }
 88+ $sStore->addNotifyMonitor($add_monitor);
 89+ }
 90+ $sStore->updateNotifyState($notify_id, 1);
 91+ wfProfileOut('SMWNotifyProcessor::enableNotify (SMW)');
 92+
 93+ return true;
 94+ }
 95+
 96+ static public function getQueryRawParams($querystring){
 97+ // read query with printouts and (possibly) other parameters like sort, order, limit, etc...
 98+ $pos = strpos($querystring, "|?");
 99+ if ($pos > 0) {
 100+ $rawparams[] = trim(substr($querystring, 0, $pos));
 101+ $ps = explode("|?", trim(substr($querystring, $pos+2)));
 102+ foreach ($ps as $param) {
 103+ $rawparams[] = "?" . trim($param);
 104+ }
 105+ } else {
 106+ $ps = preg_split('/[^\|]{1}\|{1}(?!\|)/s', $querystring);
 107+ if (count($ps) > 1) {
 108+ // last char of query condition is missing (matched with [^\|]{1}) therefore copy from original
 109+ $rawparams[] = trim(substr($querystring, 0, strlen($ps[0]) + 1));
 110+ array_shift($ps); // remove the query condition
 111+ // add other params for formating etc.
 112+ foreach ($ps as $param)
 113+ $rawparams[] = trim($param);
 114+ } // no single pipe found, no params specified in query
 115+ else $rawparams[] = trim($querystring);
 116+ }
 117+ $rawparams[] = "format=table";
 118+ $rawparams[] = "link=all";
 119+ return $rawparams;
 120+ }
 121+
 122+ static public function addNotify($rawquery, $name, $rep_all, $show_all, $delegate) {
 123+ global $wgTitle;
 124+ // Take care at least of some templates -- for better template support use #ask
 125+ $parser = new Parser();
 126+ $parserOptions = new ParserOptions();
 127+ $parser->startExternalParse( $wgTitle, $parserOptions, OT_HTML );
 128+ $rawquery = $parser->transformMsg( $rawquery, $parserOptions );
 129+
 130+ wfProfileIn('SMWNotifyProcessor::createNotify (SMW)');
 131+ $sStore = NMStorage::getDatabase();
 132+ global $wgUser;
 133+ $user_id = $wgUser->getId();
 134+ if($user_id == 0) {
 135+ wfProfileOut('SMWNotifyProcessor::createNotify (SMW)');
 136+ return "You have not logged in yet, please log in and retry again. Thanks.";
 137+ }
 138+
 139+ // check notify query first, use QueryParser from SMW
 140+ SMWQueryProcessor::processFunctionParams(SMWNotifyProcessor::getQueryRawParams($rawquery),$querystring,$params,$printouts);
 141+
 142+ $qp = new SMWQueryParser();
 143+ $qp->setDefaultNamespaces($smwgQDefaultNamespaces);
 144+ $desc = $qp->getQueryDescription($querystring);
 145+
 146+ if(count($qp->getErrors())>0) {
 147+ wfProfileOut('SMWNotifyProcessor::createNotify (SMW)');
 148+ return "Notify create failed!\n\n" . implode('\n', $qp->getErrors()) . "\n\nPlease check the query and retry again";
 149+ }
 150+
 151+ $notify_id = $sStore->addNotifyQuery($user_id, $rawquery, $name, $rep_all, $show_all, $delegate);
 152+ if($notify_id == 0) {
 153+ wfProfileOut('SMWNotifyProcessor::createNotify (SMW)');
 154+ return "Fail to save query. Please retry later...";
 155+ }
 156+ wfProfileOut('SMWNotifyProcessor::createNotify (SMW)');
 157+
 158+ wfProfileIn('SMWNotifyProcessor::enableNotify (SMW)');
 159+ $msg = '';
 160+ $result = SMWNotifyProcessor::enableNotify($notify_id, $rawquery, $msg);
 161+ wfProfileOut('SMWNotifyProcessor::enableNotify (SMW)');
 162+ return "1".($result?"1":"0")."$notify_id,$msg";
 163+ }
 164+
 165+ static public function updateStates($notify_ids) {
 166+ wfProfileIn('SMWNotifyProcessor::updateStates (SMW)');
 167+
 168+ $notifications = SMWNotifyProcessor::getNotifications();
 169+ if ($notifications == null || !is_array($notifications)) {
 170+ return "No notifications available.";
 171+ }
 172+ $result = true;
 173+ $idx = 0;
 174+ $count = count($notify_ids) - 1;
 175+ $msg = '';
 176+ $errs = '';
 177+ $sStore = NMStorage::getDatabase();
 178+ foreach ($notifications as $row) {
 179+ if(($idx<$count) && ($notify_ids[$idx]==$row['notify_id'])) {
 180+ if($row['enable']==0) {
 181+ $m = '';
 182+ $r = SMWNotifyProcessor::enableNotify($row['notify_id'], $row['query'], $m);
 183+ if(!$r) {
 184+ $msg .= "NotifyMe : '" . $row['name'] . "'$m\n\n";
 185+ $errs .= $row['notify_id'] . ",";
 186+ $result = false;
 187+ }
 188+ }
 189+ $idx ++;
 190+ } else {
 191+ if($row['enable']==1) {
 192+ $result = $sStore->disableNotifyState($row['notify_id']);
 193+ }
 194+ }
 195+ }
 196+
 197+ wfProfileOut('SMWNotifyProcessor::updateStates (SMW)');
 198+ return $result ? "States updated successfully!" : "0$errs|\n\n$msg";
 199+ }
 200+
 201+ static public function updateDelegates($delegates) {
 202+ wfProfileIn('SMWNotifyProcessor::updateDelegates (SMW)');
 203+
 204+ $notifications = SMWNotifyProcessor::getNotifications();
 205+ if ($notifications == null || !is_array($notifications)) {
 206+ return "No notifications available.";
 207+ }
 208+ $result = true;
 209+ $idx = 0;
 210+ $s = explode(':', $delegates[$idx], 2);
 211+ $count = count($delegates) - 1;
 212+ $sStore = NMStorage::getDatabase();
 213+ foreach ($notifications as $row) {
 214+ if(($idx<$count) && ($s[0]==$row['notify_id'])) {
 215+ $result = $sStore->updateDelegate($row['notify_id'], $s[1]);
 216+ $idx ++;
 217+ $s = explode(':', $delegates[$idx], 2);
 218+ } else {
 219+ $result = $sStore->updateDelegate($row['notify_id'], '');
 220+ }
 221+ if(!$result) break;
 222+ }
 223+
 224+ wfProfileOut('SMWNotifyProcessor::updateDelegates (SMW)');
 225+ return $result ? "Delegates updated successfully!" : "Delegates updated error!";
 226+ }
 227+
 228+ static public function refreshNotifyMe() {
 229+ wfProfileIn('SMWNotifyProcessor::refreshNotifyMe (SMW)');
 230+ $notifications = NMStorage::getDatabase()->getAllNotifications();
 231+ foreach ($notifications as $row) {
 232+ if($row['enable']) {
 233+ $result = SMWNotifyProcessor::enableNotify($row['notify_id'], $row['query']);
 234+ }
 235+ }
 236+ wfProfileOut('SMWNotifyProcessor::refreshNotifyMe (SMW)');
 237+ }
 238+
 239+ static public function updateReportAll($notify_ids) {
 240+ wfProfileIn('SMWNotifyProcessor::updateStates (SMW)');
 241+
 242+ $notifications = SMWNotifyProcessor::getNotifications();
 243+ if ($notifications == null || !is_array($notifications)) {
 244+ return "No notifications available.";
 245+ }
 246+ $result = true;
 247+ $idx = 0;
 248+ $count = count($notify_ids) - 1;
 249+ $sStore = NMStorage::getDatabase();
 250+ foreach ($notifications as $row) {
 251+ if(($idx<$count) && ($notify_ids[$idx]==$row['notify_id'])) {
 252+ if($row['rep_all']==0) {
 253+ $result = $sStore->updateNotifyReportAll($row['notify_id'], 1);
 254+ }
 255+ $idx ++;
 256+ } else {
 257+ if($row['rep_all']==1) {
 258+ $result = $sStore->updateNotifyReportAll($row['notify_id'], 0);
 259+ }
 260+ }
 261+ if(!$result) break;
 262+ }
 263+
 264+ wfProfileOut('SMWNotifyProcessor::updateStates (SMW)');
 265+ return $result ? "States updated successfully!" : "States updated error!";
 266+ }
 267+
 268+ static public function updateShowAll($notify_ids) {
 269+ wfProfileIn('SMWNotifyProcessor::updateStates (SMW)');
 270+
 271+ $notifications = SMWNotifyProcessor::getNotifications();
 272+ if ($notifications == null || !is_array($notifications)) {
 273+ return "No notifications available.";
 274+ }
 275+ $result = true;
 276+ $idx = 0;
 277+ $count = count($notify_ids) - 1;
 278+ $sStore = NMStorage::getDatabase();
 279+ foreach ($notifications as $row) {
 280+ if(($idx<$count) && ($notify_ids[$idx]==$row['notify_id'])) {
 281+ if($row['show_all']==0) {
 282+ $result = $sStore->updateNotifyShowAll($row['notify_id'], 1);
 283+ }
 284+ $idx ++;
 285+ } else {
 286+ if($row['show_all']==1) {
 287+ $result = $sStore->updateNotifyShowAll($row['notify_id'], 0);
 288+ }
 289+ }
 290+ if(!$result) break;
 291+ }
 292+
 293+ wfProfileOut('SMWNotifyProcessor::updateStates (SMW)');
 294+ return $result ? "States updated successfully!" : "States updated error!";
 295+ }
 296+
 297+ static public function delNotify($notify_ids) {
 298+ wfProfileIn('SMWNotifyProcessor::delNotify (SMW)');
 299+ $sStore = NMStorage::getDatabase();
 300+ $result = $sStore->removeNotifyQuery($notify_ids);
 301+ wfProfileOut('SMWNotifyProcessor::delNotify (SMW)');
 302+ return $result ? "Notification(s) deleted successfully!" : "Notification(s) deleted error!";
 303+ }
 304+
 305+ static protected $notifyJobs = array();
 306+ static public function prepareArticleSave($title) {
 307+ $page_id = $title->getArticleID();
 308+ if($page_id == 0) {
 309+ return;
 310+ }
 311+ $updates = SMWNotifyProcessor::$notifyJobs[$page_id];
 312+ if(empty($updates)) {
 313+ SMWNotifyProcessor::$notifyJobs[$page_id] = new SMWNotifyUpdate($title);
 314+ }
 315+ }
 316+ static public function articleSavedComplete($title) {
 317+ $page_id = $title->getArticleID();
 318+ if($page_id == 0) {
 319+ return;
 320+ }
 321+ $updates = SMWNotifyProcessor::$notifyJobs[$page_id];
 322+ if(empty($updates)) {
 323+ SMWNotifyProcessor::$notifyJobs[$page_id] = new SMWNotifyUpdate($title);
 324+ $updates = SMWNotifyProcessor::$notifyJobs[$page_id];
 325+ } else {
 326+ $updates->executeNotifyUpdate();
 327+ }
 328+ $updates->updateNotifyMonitor();
 329+ $updates->notifyUsers();
 330+ unset(SMWNotifyProcessor::$notifyJobs[$page_id]);
 331+ }
 332+ static public function articleDelete($title, $reason) {
 333+ $page_id = $title->getArticleID();
 334+ if($page_id == 0) {
 335+ return;
 336+ }
 337+ $updates = new SMWNotifyUpdate($title);
 338+ $updates->executeNotifyDelete($reason);
 339+ }
 340+ static public function toInfoId($type, $subquery, $attr_id) {
 341+ return base_convert(strval(($subquery << 8)|$type), 10, 9).'9'.$attr_id;
 342+ }
 343+ static public function getInfoFromId($id) {
 344+ $idx = strpos($id, '9');
 345+ $t = intval(base_convert(substr($id, 0, $idx), 9, 10));
 346+ return array(
 347+ 'type'=>$t&0xFF,
 348+ 'subquery'=>$t>>8,
 349+ 'attr_id'=>intval(substr($id, $idx+1))
 350+ );
 351+ }
 352+
 353+}
 354+
 355+// based on SMW_QueryProcessor.php (v 1.4.2)
 356+/**
 357+ * Objects of this class are in charge of parsing a query string in order
 358+ * to create an SMWDescription. The class and methods are not static in order
 359+ * to more cleanly store the intermediate state and progress of the parser.
 360+ */
 361+class SMWNotifyParser {
 362+
 363+ protected $m_sepstack; // list of open blocks ("parentheses") that need closing at current step
 364+ protected $m_curstring; // remaining string to be parsed (parsing eats query string from the front)
 365+ var $m_errors; // empty array if all went right, array of strings otherwise
 366+ protected $m_label; //label of the main query result
 367+ protected $m_defaultns; //description of the default namespace restriction, or NULL if not used
 368+
 369+ protected $m_categoryprefix; // cache label of category namespace . ':'
 370+ protected $m_conceptprefix; // cache label of concept namespace . ':'
 371+ protected $m_queryfeatures; // query features to be supported, format similar to $smwgQFeatures
 372+
 373+ // added by dch
 374+ protected $m_notify_id;
 375+ protected $m_subquery;
 376+ protected $m_printoutArticles;
 377+ public $m_result;
 378+
 379+ // modified by dch
 380+ public function SMWNotifyParser($notify_id, $printoutArticles, $queryfeatures = false) {
 381+ $this->m_notify_id = $notify_id;
 382+ $this->m_printoutArticles = $printoutArticles;
 383+ $this->m_result = true;
 384+
 385+ global $wgContLang, $smwgQFeatures;
 386+ $this->m_categoryprefix = $wgContLang->getNsText(NS_CATEGORY) . ':';
 387+ $this->m_conceptprefix = $wgContLang->getNsText(SMW_NS_CONCEPT) . ':';
 388+ $this->m_defaultns = NULL;
 389+ $this->m_queryfeatures = $queryfeatures===false?$smwgQFeatures:$queryfeatures;
 390+ }
 391+
 392+ // added by dch
 393+ public function hasSubquery() {
 394+ return $this->m_subquery > 1;
 395+ }
 396+
 397+ /**
 398+ * Provide an array of namespace constants that are used as default restrictions.
 399+ * If NULL is given, no such default restrictions will be added (faster).
 400+ */
 401+ public function setDefaultNamespaces($nsarray) {
 402+ $this->m_defaultns = NULL;
 403+ if ($nsarray !== NULL) {
 404+ foreach ($nsarray as $ns) {
 405+ $this->m_defaultns = $this->addDescription($this->m_defaultns, new SMWNamespaceDescription($ns), false);
 406+ }
 407+ }
 408+ }
 409+
 410+ /**
 411+ * Compute an SMWDescription from a query string. Returns whatever descriptions could be
 412+ * wrestled from the given string (the most general result being SMWThingDescription if
 413+ * no meaningful condition was extracted).
 414+ */
 415+ public function getQueryDescription($querystring) {
 416+ wfProfileIn('SMWNotifyParser::getQueryDescription (SMW)');
 417+ $this->m_errors = array();
 418+ $this->m_label = '';
 419+ $this->m_curstring = $querystring;
 420+ $this->m_sepstack = array();
 421+ $setNS = false;
 422+
 423+ // added by dch
 424+ $this->m_subquery = 0;
 425+
 426+ $result = $this->getSubqueryDescription($setNS, $this->m_label);
 427+ if (!$setNS) { // add default namespaces if applicable
 428+ $result = $this->addDescription($this->m_defaultns, $result);
 429+ }
 430+ if ($result === NULL) { // parsing went wrong, no default namespaces
 431+ $result = new SMWThingDescription();
 432+ }
 433+ wfProfileOut('SMWNotifyParser::getQueryDescription (SMW)');
 434+ return $result;
 435+ }
 436+
 437+ /**
 438+ * Return array of error messages (possibly empty).
 439+ */
 440+ public function getErrors() {
 441+ return $this->m_errors;
 442+ }
 443+
 444+ /**
 445+ * Return error message or empty string if no error occurred.
 446+ */
 447+ public function getErrorString() {
 448+ return smwfEncodeMessages($this->m_errors);
 449+ }
 450+
 451+ /**
 452+ * Return label for the results of this query (which
 453+ * might be empty if no such information was passed).
 454+ */
 455+ public function getLabel() {
 456+ return $this->m_label;
 457+ }
 458+
 459+
 460+ /**
 461+ * Compute an SMWDescription for current part of a query, which should
 462+ * be a standalone query (the main query or a subquery enclosed within
 463+ * "\<q\>...\</q\>". Recursively calls similar methods and returns NULL upon error.
 464+ *
 465+ * The call-by-ref parameter $setNS is a boolean. Its input specifies whether
 466+ * the query should set the current default namespace if no namespace restrictions
 467+ * were given. If false, the calling super-query is happy to set the required
 468+ * NS-restrictions by itself if needed. Otherwise the subquery has to impose the defaults.
 469+ * This is so, since outermost queries and subqueries of disjunctions will have to set
 470+ * their own default restrictions.
 471+ *
 472+ * The return value of $setNS specifies whether or not the subquery has a namespace
 473+ * specification in place. This might happen automatically if the query string imposes
 474+ * such restrictions. The return value is important for those callers that otherwise
 475+ * set up their own restrictions.
 476+ *
 477+ * Note that $setNS is no means to switch on or off default namespaces in general,
 478+ * but just controls query generation. For general effect, the default namespaces
 479+ * should be set to NULL.
 480+ *
 481+ * The call-by-ref parameter $label is used to append any label strings found.
 482+ */
 483+ protected function getSubqueryDescription(&$setNS, &$label) {
 484+ global $smwgQPrintoutLimit;
 485+ wfLoadExtensionMessages('SemanticMediaWiki');
 486+ $conjunction = NULL; // used for the current inner conjunction
 487+ $disjuncts = array(); // (disjunctive) array of subquery conjunctions
 488+ $printrequests = array(); // the printrequests found for this query level
 489+ $hasNamespaces = false; // does the current $conjnuction have its own namespace restrictions?
 490+ $mustSetNS = $setNS; // must ns restrictions be set? (may become true even if $setNS is false)
 491+
 492+ // added by dch
 493+ $subquery = $this->m_subquery;
 494+ if($subquery == 0) {
 495+ $relatedArticles = $this->m_printoutArticles;
 496+ } else {
 497+ $relatedArticles = array();
 498+ }
 499+ $this->m_subquery ++;
 500+
 501+ $continue = ($chunk = $this->readChunk()) != ''; // skip empty subquery completely, thorwing an error
 502+ while ($continue) {
 503+ $setsubNS = false;
 504+ switch ($chunk) {
 505+ case '[[': // start new link block
 506+ // modified by dch
 507+ $ld = $this->getLinkDescription($setsubNS, $label, $relatedArticles);
 508+
 509+ if ($ld instanceof SMWPrintRequest) {
 510+ $printrequests[] = $ld;
 511+ } elseif ($ld instanceof SMWDescription) {
 512+ $conjunction = $this->addDescription($conjunction,$ld);
 513+ }
 514+ break;
 515+ case '<q>': // enter new subquery, currently irrelevant but possible
 516+ $this->pushDelimiter('</q>');
 517+ $conjunction = $this->addDescription($conjunction, $this->getSubqueryDescription($setsubNS, $label));
 518+ /// TODO: print requests from subqueries currently are ignored, should be moved down
 519+ break;
 520+ case 'OR': case '||': case '': case '</q>': // finish disjunction and maybe subquery
 521+ if ($this->m_defaultns !== NULL) { // possibly add namespace restrictions
 522+ if ( $hasNamespaces && !$mustSetNS) {
 523+ // add ns restrictions to all earlier conjunctions (all of which did not have them yet)
 524+ $mustSetNS = true; // enforce NS restrictions from now on
 525+ $newdisjuncts = array();
 526+ foreach ($disjuncts as $conj) {
 527+ $newdisjuncts[] = $this->addDescription($conj, $this->m_defaultns);
 528+ }
 529+ $disjuncts = $newdisjuncts;
 530+ } elseif ( !$hasNamespaces && $mustSetNS) {
 531+ // add ns restriction to current result
 532+ $conjunction = $this->addDescription($conjunction, $this->m_defaultns);
 533+ }
 534+ }
 535+ $disjuncts[] = $conjunction;
 536+ // start anew
 537+ $conjunction = NULL;
 538+ $hasNamespaces = false;
 539+ // finish subquery?
 540+ if ($chunk == '</q>') {
 541+ if ($this->popDelimiter('</q>')) {
 542+ $continue = false; // leave the loop
 543+ } else {
 544+ $this->m_errors[] = wfMsgForContent('smw_toomanyclosing', $chunk);
 545+ return NULL;
 546+ }
 547+ } elseif ($chunk == '') {
 548+ $continue = false;
 549+ }
 550+ break;
 551+ case '+': // "... AND true" (ignore)
 552+ break;
 553+ default: // error: unexpected $chunk
 554+ $this->m_errors[] = wfMsgForContent('smw_unexpectedpart', $chunk);
 555+ //return NULL; // Try to go on, it can only get better ...
 556+ }
 557+ if ($setsubNS) { // namespace restrictions encountered in current conjunct
 558+ $hasNamespaces = true;
 559+ }
 560+ if ($continue) { // read on only if $continue remained true
 561+ $chunk = $this->readChunk();
 562+ }
 563+ }
 564+
 565+ if (count($disjuncts) > 0) { // make disjunctive result
 566+ $result = NULL;
 567+ foreach ($disjuncts as $d) {
 568+ if ($d === NULL) {
 569+ $this->m_errors[] = wfMsgForContent('smw_emptysubquery');
 570+ $setNS = false;
 571+ return NULL;
 572+ } else {
 573+ $result = $this->addDescription($result, $d, false);
 574+ }
 575+ }
 576+ } else {
 577+ $this->m_errors[] = wfMsgForContent('smw_emptysubquery');
 578+ $setNS = false;
 579+ return NULL;
 580+ }
 581+ $setNS = $mustSetNS; // NOTE: also false if namespaces were given but no default NS descs are available
 582+
 583+ $prcount = 0;
 584+ foreach ($printrequests as $pr) { // add printrequests
 585+ if ($prcount < $smwgQPrintoutLimit) {
 586+ $result->addPrintRequest($pr);
 587+ $prcount++;
 588+ } else {
 589+ $this->m_errors[] = wfMsgForContent('smw_overprintoutlimit');
 590+ break;
 591+ }
 592+ }
 593+
 594+ // added by dch
 595+ if($this->m_result) {
 596+ $sStore = NMStorage::getDatabase();
 597+ $this->m_result = $sStore->addNotifyRelations($this->m_notify_id, $relatedArticles, $subquery);
 598+ }
 599+
 600+ return $result;
 601+ }
 602+
 603+ /**
 604+ * Compute an SMWDescription for current part of a query, which should
 605+ * be the content of "[[ ... ]]". Alternatively, if the current syntax
 606+ * specifies a print request, return the print request object.
 607+ * Returns NULL upon error.
 608+ *
 609+ * Parameters $setNS and $label have the same use as in getSubqueryDescription().
 610+ */
 611+ // modified by dch, add $relatedArticles
 612+ protected function getLinkDescription(&$setNS, &$label, &$relatedArticles) {
 613+ // This method is called when we encountered an opening '[['. The following
 614+ // block could be a Category-statement, fixed object, property statements,
 615+ // or according print statements.
 616+ $chunk = $this->readChunk('',true,false); // NOTE: untrimmed, initial " " escapes prop. chains
 617+ if ( (smwfNormalTitleText($chunk) == $this->m_categoryprefix) || // category statement or
 618+ (smwfNormalTitleText($chunk) == $this->m_conceptprefix) ) { // concept statement
 619+ return $this->getClassDescription($setNS, $label, $relatedArticles,
 620+ (smwfNormalTitleText($chunk) == $this->m_categoryprefix));
 621+ } else { // fixed subject, namespace restriction, property query, or subquery
 622+ $sep = $this->readChunk('',false); //do not consume hit, "look ahead"
 623+ if ( ($sep == '::') || ($sep == ':=') ) {
 624+ if ($chunk{0} !=':') { // property statement
 625+ return $this->getPropertyDescription($chunk, $setNS, $label, $relatedArticles);
 626+ } else { // escaped article description, read part after :: to get full contents
 627+ $chunk .= $this->readChunk('\[\[|\]\]|\|\||\|');
 628+ return $this->getArticleDescription(trim($chunk), $setNS, $label, $relatedArticles);
 629+ }
 630+ } else { // Fixed article/namespace restriction. $sep should be ]] or ||
 631+ return $this->getArticleDescription(trim($chunk), $setNS, $label, $relatedArticles);
 632+ }
 633+ }
 634+ }
 635+
 636+ /**
 637+ * Parse a category description (the part of an inline query that
 638+ * is in between "[[Category:" and the closing "]]" and create a
 639+ * suitable description.
 640+ */
 641+ // modified by dch ,add $relatedArticles
 642+ protected function getClassDescription(&$setNS, &$label, &$relatedArticles, $category=true) {
 643+ global $smwgSMWBetaCompatible; // * printouts only for this old version
 644+ // note: no subqueries allowed here, inline disjunction allowed, wildcards allowed
 645+ $result = NULL;
 646+ $continue = true;
 647+ while ($continue) {
 648+ $chunk = $this->readChunk();
 649+ if ($chunk == '+') {
 650+ //wildcard, ignore for categories (semantically meaningless, everything is in some class)
 651+ } elseif ( ($chunk == '+') && $category && $smwgSMWBetaCompatible) { // print statement
 652+ $chunk = $this->readChunk('\]\]|\|');
 653+ if ($chunk == '|') {
 654+ $printlabel = $this->readChunk('\]\]');
 655+ if ($printlabel != ']]') {
 656+ $chunk = $this->readChunk('\]\]');
 657+ } else {
 658+ $printlabel = '';
 659+ $chunk = ']]';
 660+ }
 661+ } else {
 662+ global $wgContLang;
 663+ $printlabel = $wgContLang->getNSText(NS_CATEGORY);
 664+ }
 665+ if ($chunk == ']]') {
 666+ return new SMWPrintRequest(SMWPrintRequest::PRINT_CATS, $printlabel);
 667+ } else {
 668+ $this->m_errors[] = wfMsgForContent('smw_badprintout');
 669+ return NULL;
 670+ }
 671+ } else { //assume category/concept title
 672+ /// NOTE: use m_c...prefix to prevent problems with, e.g., [[Category:Template:Test]]
 673+ $class = Title::newFromText(($category?$this->m_categoryprefix:$this->m_conceptprefix) . $chunk);
 674+ if ($class !== NULL) {
 675+ $desc = $category?new SMWClassDescription($class):new SMWConceptDescription($class);
 676+ $result = $this->addDescription($result, $desc, false);
 677+ }
 678+
 679+ // added by dch
 680+ if($category) {
 681+ $relatedArticles[] = array(
 682+ 'namespace' => NS_CATEGORY,
 683+ 'title' => $class->getDBkey());
 684+ }
 685+
 686+ }
 687+ $chunk = $this->readChunk();
 688+ $continue = ($chunk == '||') && $category; // disjunctions only for cateories
 689+ }
 690+
 691+ return $this->finishLinkDescription($chunk, false, $result, $setNS, $label);
 692+ }
 693+
 694+ /**
 695+ * Parse a property description (the part of an inline query that
 696+ * is in between "[[Some property::" and the closing "]]" and create a
 697+ * suitable description. The "::" is the first chunk on the current
 698+ * string.
 699+ */
 700+ // modified by dch ,add $relatedArticles
 701+ protected function getPropertyDescription($propertyname, &$setNS, &$label, &$relatedArticles) {
 702+ global $smwgSMWBetaCompatible; // support for old * printouts of beta
 703+ wfLoadExtensionMessages('SemanticMediaWiki');
 704+ $this->readChunk(); // consume separator ":=" or "::"
 705+ // first process property chain syntax (e.g. "property1.property2::value"):
 706+ if ($propertyname{0} == ' ') { // escape
 707+ $propertynames = array($propertyname);
 708+ } else {
 709+ $propertynames = explode('.', $propertyname);
 710+ }
 711+ $properties = array();
 712+ $typeid = '_wpg';
 713+ foreach ($propertynames as $name) {
 714+ if ($typeid != '_wpg') { // non-final property in chain was no wikipage: not allowed
 715+ $this->m_errors[] = wfMsgForContent('smw_valuesubquery', $prevname);
 716+ return NULL; ///TODO: read some more chunks and try to finish [[ ]]
 717+ }
 718+ $property = SMWPropertyValue::makeUserProperty($name);
 719+ if (!$property->isValid()) { // illegal property identifier
 720+ $this->m_errors = array_merge($this->m_errors, $property->getErrors());
 721+ return NULL; ///TODO: read some more chunks and try to finish [[ ]]
 722+ }
 723+ $typeid = $property->getTypeID();
 724+ $prevname = $name;
 725+ $properties[] = $property;
 726+
 727+ // added by dch
 728+ $relatedArticles[] = array(
 729+ 'namespace' => SMW_NS_PROPERTY,
 730+ 'title' => $property->getDBkey());
 731+
 732+ } ///NOTE: after iteration, $property and $typeid correspond to last value
 733+
 734+ $innerdesc = NULL;
 735+ $continue = true;
 736+ while ($continue) {
 737+ $chunk = $this->readChunk();
 738+ switch ($chunk) {
 739+ case '+': // wildcard, add namespaces for page-type properties
 740+ if ( ($this->m_defaultns !== NULL) && ($typeid == '_wpg') ) {
 741+ $innerdesc = $this->addDescription($innerdesc, $this->m_defaultns, false);
 742+ } else {
 743+ $innerdesc = $this->addDescription($innerdesc, new SMWThingDescription(), false);
 744+ }
 745+ $chunk = $this->readChunk();
 746+ break;
 747+ case '<q>': // subquery, set default namespaces
 748+ if ($typeid == '_wpg') {
 749+ $this->pushDelimiter('</q>');
 750+ $setsubNS = true;
 751+ $sublabel = '';
 752+ $innerdesc = $this->addDescription($innerdesc, $this->getSubqueryDescription($setsubNS, $sublabel), false);
 753+ } else { // no subqueries allowed for non-pages
 754+ $this->m_errors[] = wfMsgForContent('smw_valuesubquery', end($propertynames));
 755+ $innerdesc = $this->addDescription($innerdesc, new SMWThingDescription(), false);
 756+ }
 757+ $chunk = $this->readChunk();
 758+ break;
 759+ default: //normal object value or print statement
 760+ // read value(s), possibly with inner [[...]]
 761+ $open = 1;
 762+ $value = $chunk;
 763+ $continue2 = true;
 764+ // read value with inner [[, ]], ||
 765+ while ( ($open > 0) && ($continue2) ) {
 766+ $chunk = $this->readChunk('\[\[|\]\]|\|\||\|');
 767+ switch ($chunk) {
 768+ case '[[': // open new [[ ]]
 769+ $open++;
 770+ break;
 771+ case ']]': // close [[ ]]
 772+ $open--;
 773+ break;
 774+ case '|': case '||': // terminates only outermost [[ ]]
 775+ if ($open == 1) {
 776+ $open = 0;
 777+ }
 778+ break;
 779+ case '': ///TODO: report error; this is not good right now
 780+ $continue2 = false;
 781+ break;
 782+ }
 783+ if ($open != 0) {
 784+ $value .= $chunk;
 785+ }
 786+ } ///NOTE: at this point, we normally already read one more chunk behind the value
 787+
 788+ if ($typeid == '__nry') { // nary value
 789+ $dv = SMWDataValueFactory::newPropertyObjectValue($property);
 790+ $dv->acceptQuerySyntax();
 791+ $dv->setUserValue($value);
 792+ $vl = $dv->getValueList();
 793+ $pm = $dv->getPrintModifier();
 794+ if ($vl !== NULL) { // prefer conditions over print statements (only one possible right now)
 795+ $innerdesc = $this->addDescription($innerdesc, $vl, false);
 796+ } elseif ($pm !== false) {
 797+ if ($chunk == '|') {
 798+ $printlabel = $this->readChunk('\]\]');
 799+ if ($printlabel != ']]') {
 800+ $chunk = $this->readChunk('\]\]');
 801+ } else {
 802+ $printlabel = '';
 803+ $chunk = ']]';
 804+ }
 805+ } else {
 806+ $printlabel = $property->getWikiValue();
 807+ }
 808+ if ($chunk == ']]') {
 809+ return new SMWPrintRequest(SMWPrintRequest::PRINT_PROP, $printlabel, $property, $pm);
 810+ } else {
 811+ $this->m_errors[] = wfMsgForContent('smw_badprintout');
 812+ return NULL;
 813+ }
 814+ }
 815+ } else { // unary value
 816+ $comparator = SMW_CMP_EQ;
 817+ $printmodifier = '';
 818+ SMWNotifyParser::prepareValue($value, $comparator, $printmodifier);
 819+ if ( ($value == '*') && $smwgSMWBetaCompatible ) {
 820+ if ($chunk == '|') {
 821+ $printlabel = $this->readChunk('\]\]');
 822+ if ($printlabel != ']]') {
 823+ $chunk = $this->readChunk('\]\]');
 824+ } else {
 825+ $printlabel = '';
 826+ $chunk = ']]';
 827+ }
 828+ } else {
 829+ $printlabel = $property->getWikiValue();
 830+ }
 831+ if ($chunk == ']]') {
 832+ return new SMWPrintRequest(SMWPrintRequest::PRINT_PROP, $printlabel, $property, $printmodifier);
 833+ } else {
 834+ $this->m_errors[] = wfMsgForContent('smw_badprintout');
 835+ return NULL;
 836+ }
 837+ } else {
 838+ $dv = SMWDataValueFactory::newPropertyObjectValue($property, $value);
 839+ if (!$dv->isValid()) {
 840+ $this->m_errors = $this->m_errors + $dv->getErrors();
 841+ $vd = new SMWThingDescription();
 842+ } else {
 843+ $vd = new SMWValueDescription($dv, $comparator);
 844+ }
 845+ $innerdesc = $this->addDescription($innerdesc, $vd, false);
 846+ }
 847+ }
 848+ }
 849+ $continue = ($chunk == '||');
 850+ }
 851+
 852+ if ($innerdesc === NULL) { // make a wildcard search
 853+ if ( ($this->m_defaultns !== NULL) && ($typeid == '_wpg') ) {
 854+ $innerdesc = $this->addDescription($innerdesc, $this->m_defaultns, false);
 855+ } else {
 856+ $innerdesc = $this->addDescription($innerdesc, new SMWThingDescription(), false);
 857+ }
 858+ $this->m_errors[] = wfMsgForContent('smw_propvalueproblem', $property->getWikiValue());
 859+ }
 860+ $properties = array_reverse($properties);
 861+ foreach ($properties as $property) {
 862+ $innerdesc = new SMWSomeProperty($property,$innerdesc);
 863+ }
 864+ $result = $innerdesc;
 865+ return $this->finishLinkDescription($chunk, false, $result, $setNS, $label);
 866+ }
 867+
 868+
 869+ /**
 870+ * Prepare a single value string, possibly extracting comparators and
 871+ * printmodifier. $value is changed to consist only of the remaining
 872+ * effective value string, or of "*" for print statements.
 873+ */
 874+ static public function prepareValue(&$value, &$comparator, &$printmodifier) {
 875+ global $smwgQComparators, $smwgSMWBetaCompatible; // support for old * printouts of beta
 876+ // get print modifier behind *
 877+ if ($smwgSMWBetaCompatible) {
 878+ $list = preg_split('/^\*/',$value,2);
 879+ if (count($list) == 2) { //hit
 880+ $value = '*';
 881+ $printmodifier = $list[1];
 882+ } else {
 883+ $printmodifier = '';
 884+ }
 885+ if ($value == '*') { // printout statement
 886+ return;
 887+ }
 888+ }
 889+ $list = preg_split('/^(' . $smwgQComparators . ')/u',$value, 2, PREG_SPLIT_DELIM_CAPTURE);
 890+ $comparator = SMW_CMP_EQ;
 891+ if (count($list) == 3) { // initial comparator found ($list[1] should be empty)
 892+ switch ($list[1]) {
 893+ case '<':
 894+ $comparator = SMW_CMP_LEQ;
 895+ $value = $list[2];
 896+ break;
 897+ case '>':
 898+ $comparator = SMW_CMP_GEQ;
 899+ $value = $list[2];
 900+ break;
 901+ case '!':
 902+ $comparator = SMW_CMP_NEQ;
 903+ $value = $list[2];
 904+ break;
 905+ case '~':
 906+ $comparator = SMW_CMP_LIKE;
 907+ $value = $list[2];
 908+ break;
 909+ //default: not possible
 910+ }
 911+ }
 912+ }
 913+
 914+ /**
 915+ * Parse an article description (the part of an inline query that
 916+ * is in between "[[" and the closing "]]" assuming it is not specifying
 917+ * a category or property) and create a suitable description.
 918+ * The first chunk behind the "[[" has already been read and is
 919+ * passed as a parameter.
 920+ */
 921+ // modified by dch ,add $relatedArticles
 922+ protected function getArticleDescription($firstchunk, &$setNS, &$label, &$relatedArticles) {
 923+ wfLoadExtensionMessages('SemanticMediaWiki');
 924+ $chunk = $firstchunk;
 925+ $result = NULL;
 926+ $continue = true;
 927+ //$innerdesc = NULL;
 928+ while ($continue) {
 929+ if ($chunk == '<q>') { // no subqueries of the form [[<q>...</q>]] (not needed)
 930+ $this->m_errors[] = wfMsgForContent('smw_misplacedsubquery');
 931+ return NULL;
 932+ }
 933+ $list = preg_split('/:/', $chunk, 3); // ":Category:Foo" "User:bar" ":baz" ":+"
 934+ if ( ($list[0] == '') && (count($list)==3) ) {
 935+ $list = array_slice($list, 1);
 936+ }
 937+ if ( (count($list) == 2) && ($list[1] == '+') ) { // try namespace restriction
 938+ global $wgContLang;
 939+ $idx = $wgContLang->getNsIndex($list[0]);
 940+ if ($idx !== false) {
 941+ $result = $this->addDescription($result, new SMWNamespaceDescription($idx), false);
 942+ }
 943+ } else {
 944+ $value = SMWDataValueFactory::newTypeIDValue('_wpg', $chunk);
 945+ if ($value->isValid()) {
 946+ $result = $this->addDescription($result, new SMWValueDescription($value), false);
 947+ // added by dch
 948+ $relatedArticles[] = array(
 949+ 'namespace' => NS_MAIN,
 950+ 'title' => Title::makeTitle( NS_MAIN, $chunk )->getDBkey());
 951+ }
 952+ }
 953+
 954+ $chunk = $this->readChunk('\[\[|\]\]|\|\||\|');
 955+ if ($chunk == '||') {
 956+ $chunk = $this->readChunk('\[\[|\]\]|\|\||\|');
 957+ $continue = true;
 958+ } else {
 959+ $continue = false;
 960+ }
 961+ }
 962+
 963+ return $this->finishLinkDescription($chunk, true, $result, $setNS, $label);
 964+ }
 965+
 966+ protected function finishLinkDescription($chunk, $hasNamespaces, $result, &$setNS, &$label) {
 967+ wfLoadExtensionMessages('SemanticMediaWiki');
 968+ if ($result === NULL) { // no useful information or concrete error found
 969+ $this->m_errors[] = wfMsgForContent('smw_badqueryatom');
 970+ } elseif (!$hasNamespaces && $setNS && ($this->m_defaultns !== NULL) ) {
 971+ $result = $this->addDescription($result, $this->m_defaultns);
 972+ $hasNamespaces = true;
 973+ }
 974+ $setNS = $hasNamespaces;
 975+
 976+ // terminate link (assuming that next chunk was read already)
 977+ if ($chunk == '|') {
 978+ $chunk = $this->readChunk('\]\]');
 979+ if ($chunk != ']]') {
 980+ $label .= $chunk;
 981+ $chunk = $this->readChunk('\]\]');
 982+ } else { // empty label does not add to overall label
 983+ $chunk = ']]';
 984+ }
 985+ }
 986+ if ($chunk != ']]') {
 987+ // What happended? We found some chunk that could not be processed as
 988+ // link content (as in [[Category:Test<q>]]) and there was no label to
 989+ // eat it. Or the closing ]] are just missing entirely.
 990+ if ($chunk != '') {
 991+ $this->m_errors[] = wfMsgForContent('smw_misplacedsymbol', htmlspecialchars($chunk));
 992+ // try to find a later closing ]] to finish this misshaped subpart
 993+ $chunk = $this->readChunk('\]\]');
 994+ if ($chunk != ']]') {
 995+ $chunk = $this->readChunk('\]\]');
 996+ }
 997+ }
 998+ if ($chunk == '') {
 999+ $this->m_errors[] = wfMsgForContent('smw_noclosingbrackets');
 1000+ }
 1001+ }
 1002+ return $result;
 1003+ }
 1004+
 1005+ /**
 1006+ * Get the next unstructured string chunk from the query string.
 1007+ * Chunks are delimited by any of the special strings used in inline queries
 1008+ * (such as [[, ]], <q>, ...). If the string starts with such a delimiter,
 1009+ * this delimiter is returned. Otherwise the first string in front of such a
 1010+ * delimiter is returned.
 1011+ * Trailing and initial spaces are ignored if $trim is true, and chunks
 1012+ * consisting only of spaces are not returned.
 1013+ * If there is no more qurey string left to process, the empty string is
 1014+ * returned (and in no other case).
 1015+ *
 1016+ * The stoppattern can be used to customise the matching, especially in order to
 1017+ * overread certain special symbols.
 1018+ *
 1019+ * $consume specifies whether the returned chunk should be removed from the
 1020+ * query string.
 1021+ */
 1022+ protected function readChunk($stoppattern = '', $consume=true, $trim=true) {
 1023+ if ($stoppattern == '') {
 1024+ $stoppattern = '\[\[|\]\]|::|:=|<q>|<\/q>|^' . $this->m_categoryprefix .
 1025+ '|^' . $this->m_conceptprefix . '|\|\||\|';
 1026+ }
 1027+ $chunks = preg_split('/[\s]*(' . $stoppattern . ')/u', $this->m_curstring, 2, PREG_SPLIT_DELIM_CAPTURE);
 1028+ if (count($chunks) == 1) { // no matches anymore, strip spaces and finish
 1029+ if ($consume) {
 1030+ $this->m_curstring = '';
 1031+ }
 1032+ return $trim?trim($chunks[0]):$chunks[0];
 1033+ } elseif (count($chunks) == 3) { // this should generally happen if count is not 1
 1034+ if ($chunks[0] == '') { // string started with delimiter
 1035+ if ($consume) {
 1036+ $this->m_curstring = $chunks[2];
 1037+ }
 1038+ return $trim?trim($chunks[1]):$chunks[1];
 1039+ } else {
 1040+ if ($consume) {
 1041+ $this->m_curstring = $chunks[1] . $chunks[2];
 1042+ }
 1043+ return $trim?trim($chunks[0]):$chunks[0];
 1044+ }
 1045+ } else { return false; } //should never happen
 1046+ }
 1047+
 1048+ /**
 1049+ * Enter a new subblock in the query, which must at some time be terminated by the
 1050+ * given $endstring delimiter calling popDelimiter();
 1051+ */
 1052+ protected function pushDelimiter($endstring) {
 1053+ array_push($this->m_sepstack, $endstring);
 1054+ }
 1055+
 1056+ /**
 1057+ * Exit a subblock in the query ending with the given delimiter.
 1058+ * If the delimiter does not match the top-most open block, false
 1059+ * will be returned. Otherwise return true.
 1060+ */
 1061+ protected function popDelimiter($endstring) {
 1062+ $topdelim = array_pop($this->m_sepstack);
 1063+ return ($topdelim == $endstring);
 1064+ }
 1065+
 1066+ /**
 1067+ * Extend a given description by a new one, either by adding the new description
 1068+ * (if the old one is a container description) or by creating a new container.
 1069+ * The parameter $conjunction determines whether the combination of both descriptions
 1070+ * should be a disjunction or conjunction.
 1071+ *
 1072+ * In the special case that the current description is NULL, the new one will just
 1073+ * replace the current one.
 1074+ *
 1075+ * The return value is the expected combined description. The object $curdesc will
 1076+ * also be changed (if it was non-NULL).
 1077+ */
 1078+ protected function addDescription($curdesc, $newdesc, $conjunction = true) {
 1079+ wfLoadExtensionMessages('SemanticMediaWiki');
 1080+ $notallowedmessage = 'smw_noqueryfeature';
 1081+ if ($newdesc instanceof SMWSomeProperty) {
 1082+ $allowed = $this->m_queryfeatures & SMW_PROPERTY_QUERY;
 1083+ } elseif ($newdesc instanceof SMWClassDescription) {
 1084+ $allowed = $this->m_queryfeatures & SMW_CATEGORY_QUERY;
 1085+ } elseif ($newdesc instanceof SMWConceptDescription) {
 1086+ $allowed = $this->m_queryfeatures & SMW_CONCEPT_QUERY;
 1087+ } elseif ($newdesc instanceof SMWConjunction) {
 1088+ $allowed = $this->m_queryfeatures & SMW_CONJUNCTION_QUERY;
 1089+ $notallowedmessage = 'smw_noconjunctions';
 1090+ } elseif ($newdesc instanceof SMWDisjunction) {
 1091+ $allowed = $this->m_queryfeatures & SMW_DISJUNCTION_QUERY;
 1092+ $notallowedmessage = 'smw_nodisjunctions';
 1093+ } else {
 1094+ $allowed = true;
 1095+ }
 1096+ if (!$allowed) {
 1097+ $this->m_errors[] = wfMsgForContent($notallowedmessage, str_replace('[', '&#x005B;', $newdesc->getQueryString()));
 1098+ return $curdesc;
 1099+ }
 1100+ if ($newdesc === NULL) {
 1101+ return $curdesc;
 1102+ } elseif ($curdesc === NULL) {
 1103+ return $newdesc;
 1104+ } else { // we already found descriptions
 1105+ if ( (($conjunction) && ($curdesc instanceof SMWConjunction)) ||
 1106+ ((!$conjunction) && ($curdesc instanceof SMWDisjunction)) ) { // use existing container
 1107+ $curdesc->addDescription($newdesc);
 1108+ return $curdesc;
 1109+ } elseif ($conjunction) { // make new conjunction
 1110+ if ($this->m_queryfeatures & SMW_CONJUNCTION_QUERY) {
 1111+ return new SMWConjunction(array($curdesc,$newdesc));
 1112+ } else {
 1113+ $this->m_errors[] = wfMsgForContent('smw_noconjunctions', str_replace('[', '&#x005B;', $newdesc->getQueryString()));
 1114+ return $curdesc;
 1115+ }
 1116+ } else { // make new disjunction
 1117+ if ($this->m_queryfeatures & SMW_DISJUNCTION_QUERY) {
 1118+ return new SMWDisjunction(array($curdesc,$newdesc));
 1119+ } else {
 1120+ $this->m_errors[] = wfMsgForContent('smw_nodisjunctions', str_replace('[', '&#x005B;', $newdesc->getQueryString()));
 1121+ return $curdesc;
 1122+ }
 1123+ }
 1124+ }
 1125+ }
 1126+}
 1127+
 1128+class SMWNotifyUpdate {
 1129+ protected $m_info;
 1130+ protected $m_title;
 1131+ protected $m_userMsgs;
 1132+ protected $m_userHtmlPropMsgs;
 1133+ protected $m_userHtmlNMMsgs;
 1134+ protected $m_userNMs;
 1135+ protected $m_notifyHtmlPropMsgs;
 1136+ protected $m_notifyHtmlMsgs;
 1137+ protected $m_newMonitor;
 1138+ protected $m_removeMonitored;
 1139+ protected $m_subQueryNotify;
 1140+
 1141+ protected $m_linker;
 1142+
 1143+ protected function getSemanticInfo($title) {
 1144+ $result = array();
 1145+ $sStore = NMStorage::getDatabase();
 1146+ $semdata = smwfGetStore()->getSemanticData($title);
 1147+ foreach($semdata->getProperties() as $property) {
 1148+ if (!$property->isShown() && $property->getWikiValue()!='') { // showing this is not desired, hide
 1149+ continue;
 1150+ } elseif ($property->isUserDefined()) { // user defined property
 1151+ $property->setCaption(preg_replace('/[ ]/u','&nbsp;',$property->getWikiValue(),2));
 1152+ }
 1153+
 1154+ $propvalues = $semdata->getPropertyValues($property);
 1155+ if($property->getWikiValue()!='') {
 1156+ foreach ($propvalues as $propvalue) {
 1157+ if($propvalue->getXSDValue() != '') {
 1158+ $result[SMWNotifyProcessor::toInfoId(2,0,$sStore->lookupSmwId(SMW_NS_PROPERTY, $property->getXSDValue()))][] = array('name'=>$property, 'value'=>$propvalue);
 1159+ }
 1160+ }
 1161+ } else {
 1162+ foreach ($propvalues as $propvalue) {
 1163+ if(($propvalue instanceof SMWWikiPageValue)&&($propvalue->getNamespace() == NS_CATEGORY)) {
 1164+ $result[SMWNotifyProcessor::toInfoId(0,0,$sStore->lookupSmwId(NS_CATEGORY, $propvalue->getXSDValue()))][] = array('name'=>$propvalue, 'value'=>null);
 1165+ }
 1166+ }
 1167+ }
 1168+ }
 1169+ return $result;
 1170+ }
 1171+
 1172+ public function SMWNotifyUpdate($title) {
 1173+ $this->m_title = $title;
 1174+ $this->m_userMsgs = array();
 1175+ $this->m_userHtmlPropMsgs = array();
 1176+ $this->m_userHtmlNMMsgs = array();
 1177+ $this->m_userNMs = array();
 1178+ $this->m_notifyHtmlPropMsgs = array();
 1179+ $this->m_notifyHtmlMsgs = array();
 1180+ $this->m_newMonitor = array();
 1181+ $this->m_removeMonitored = array();
 1182+ $this->m_subQueryNotify = array();
 1183+ $this->m_linker = new Linker();
 1184+
 1185+ $page_id = $title->getArticleID();
 1186+ if(($page_id == 0) || ($this->m_title->getNamespace() != NS_MAIN)) {
 1187+ return;
 1188+ }
 1189+ $this->m_info = $this->getSemanticInfo($this->m_title);
 1190+ }
 1191+ public function executeNotifyDelete($reason) {
 1192+ $page_id = $this->m_title->getArticleID();
 1193+ if(($page_id == 0) || ($this->m_title->getNamespace() != NS_MAIN)) {
 1194+ return;
 1195+ }
 1196+ $page_name = $this->m_title->getText().' ('.$this->m_title->getFullUrl().')';
 1197+ $page_html_name = '<a href="'.$this->m_title->getFullUrl().'">'.htmlspecialchars($this->m_title->getText()).'</a>';
 1198+
 1199+ $msg .= "\r\nPage $page_name has been deleted.\r\nReason : $reason";
 1200+ $sStore = NMStorage::getDatabase();
 1201+ $notifications = $sStore->getMonitoredNotifications($page_id);
 1202+
 1203+ foreach($notifications as $user_id=>$notifies) {
 1204+ $this->m_userMsgs[$user_id] .= $msg.'\r\n( NM: ';
 1205+ $hint = "<P>Page $page_html_name has been deleted.<br/>Reason : <font color='red'>".htmlspecialchars($reason)."</font></P>";
 1206+ $this->m_userHtmlNMMsgs[$user_id] .= "$hint<P>Notify Me: ";
 1207+ $idx = 0;
 1208+ foreach($notifies as $notify_id=>$notify_detail) {
 1209+ if($idx>0) {
 1210+ $this->m_userMsgs[$user_id] .= ', ';
 1211+ $this->m_userHtmlNMMsgs[$user_id] .= ', ';
 1212+ }
 1213+ $this->m_notifyHtmlMsgs[$notify_id] .= $hint;
 1214+
 1215+ $this->m_userMsgs[$user_id] .= $notify_detail['name'];
 1216+ $this->m_userHtmlNMMsgs[$user_id] .= '<b>'.htmlspecialchars($notify_detail['name']).'</b>';
 1217+ $idx++;
 1218+ $this->m_removeMonitored[] = array('notify_id'=>$notify_id, 'page_id'=>$page_id);
 1219+
 1220+ $this->m_userNMs[$user_id][] = $notify_id;
 1221+ }
 1222+
 1223+ $this->m_userMsgs[$user_id] .= " ).";
 1224+ $this->m_userHtmlNMMsgs[$user_id] .= " does not match this page now.</P>";
 1225+ }
 1226+
 1227+ $this->notifyUsers();
 1228+ $sStore->removeNotifyMonitor($this->m_removeMonitored);
 1229+ }
 1230+ function isEqual($v1, $v2) {
 1231+ return (strval($v1[value]->getXSDValue()) == strval($v2[value]->getXSDValue()));
 1232+ }
 1233+ function getNotifyPlain($info, $key){
 1234+ $i = SMWNotifyProcessor::getInfoFromId($key);
 1235+ if($i[type]==0) {
 1236+ return "\r\n'".$info[name]->getWikiValue()."' has been ".($info[sem_act]==0?"deleted":"added").".";
 1237+ } else {
 1238+ $tmp = "\r\nProperty '".$info[name]->getWikiValue()."' has been ".($info[sem_act]==0?"deleted.":($info[sem_act]==1?"modified":"added")).".";
 1239+ $first = true;
 1240+ foreach($info[del_vals] as $val) {
 1241+ if($first) {
 1242+ $tmp .= "\r\nValue '";
 1243+ $first = false;
 1244+ } else {
 1245+ $tmp .= "', '";
 1246+ }
 1247+ $tmp .= $val[plain];
 1248+ }
 1249+ if(!$first) {
 1250+ $tmp .= "' deleted.";
 1251+ }
 1252+ $first = true;
 1253+ foreach($info[new_vals] as $val) {
 1254+ if($first) {
 1255+ $tmp .= "\r\nValue '";
 1256+ $first = false;
 1257+ } else {
 1258+ $tmp .= "', '";
 1259+ }
 1260+ $tmp .= $val[plain];
 1261+ }
 1262+ if(!$first) {
 1263+ $tmp .= "' added.";
 1264+ }
 1265+ return $tmp."\r\n";
 1266+ }
 1267+ }
 1268+ function getNotifyHtml($info, $key){
 1269+ $i = SMWNotifyProcessor::getInfoFromId($key);
 1270+ if($i[type]==0) {
 1271+ return "<td>Category</td>
 1272+ <td>".$info[name]->getShortHTMLText($this->m_linker)."</td>
 1273+ <td>".($info[sem_act]==0?"<font color='green'>remove</font>":"<font color='red'>cite</font>")."</td>
 1274+ <td colspan='2'>N/A</td>";
 1275+ } else {
 1276+ $rows = max(count($info[del_vals]), count($info[new_vals]));
 1277+ $tmp = "<tr><td rowspan='$rows'>Property</td>
 1278+ <td rowspan='$rows'>".$info[name]->getShortHTMLText($this->m_linker)."</td>
 1279+ <td rowspan='$rows'>".($info[sem_act]==0?"<font color='green'>remove</font>":($info[sem_act]==1?"<font color='blue'>modify</font>":"<font color='red'>cite</font>"))."</td>";
 1280+ for($idx=0;$idx<$rows;++$idx) {
 1281+ if($idx>0){
 1282+ $tmp .= "<tr>";
 1283+ }
 1284+ $tmp .= "<td>".(isset($info[del_vals][$idx])?$info[del_vals][$idx][html]:"&nbsp;")."</td>
 1285+ <td>".(isset($info[new_vals][$idx])?$info[new_vals][$idx][html]:"&nbsp;")."</td>
 1286+ </tr>";
 1287+ }
 1288+ return $tmp;
 1289+ }
 1290+ }
 1291+ public function executeNotifyUpdate() {
 1292+ $page_id = $this->m_title->getArticleID();
 1293+ if(($page_id == 0) || ($this->m_title->getNamespace() != NS_MAIN)) {
 1294+ return;
 1295+ }
 1296+ $sStore = NMStorage::getDatabase();
 1297+
 1298+ $info = $this->getSemanticInfo($this->m_title);
 1299+ // get different
 1300+ $tmp_info = array(); // type : category 0, property 2; name; sem action : del 0, modify 1, add 2; val action : del 0, add 1
 1301+ foreach($this->m_info as $key=>$value) {
 1302+ $i = SMWNotifyProcessor::getInfoFromId($key);
 1303+ $updated = false;
 1304+ if(!isset($info[$key])) {
 1305+ if($i[type]==0) {
 1306+ $tmp_info[$key] = array('sem_act' => 0, 'name'=>$value[0][name]);
 1307+ }else {
 1308+ $tmp_info[$key] = array('sem_act'=>0, 'name'=>$value[0][name], 'del_vals'=>array(), 'new_vals'=>array());
 1309+ foreach($value as $v) {
 1310+ $tmp_info[$key][del_vals][] = array( 'plain' => $v[value]->getWikiValue(), 'html' => $v[value]->getShortHTMLText($this->m_linker));
 1311+ }
 1312+ }
 1313+ } else if($i[type] == 2) {
 1314+ $mvalue = $info[$key];
 1315+ foreach($value as $v1) {
 1316+ $found = false;
 1317+ foreach($mvalue as $v2) {
 1318+ if($this->isEqual($v1, $v2)) {
 1319+ $found = true;
 1320+ break;
 1321+ }
 1322+ }
 1323+ if(!$found) {
 1324+ if(!$updated){
 1325+ $updated = true;
 1326+ $tmp_info[$key] = array('sem_act'=>1, 'name'=>$value[0][name], 'del_vals'=>array(), 'new_vals'=>array());
 1327+ }
 1328+ $tmp_info[$key][del_vals][] = array( 'plain' => $v1[value]->getWikiValue(), 'html' => $v1[value]->getShortHTMLText($this->m_linker));
 1329+ }
 1330+ }
 1331+ foreach($mvalue as $v1) {
 1332+ $found = false;
 1333+ foreach($value as $v2) {
 1334+ if($this->isEqual($v1, $v2)) {
 1335+ $found = true;
 1336+ break;
 1337+ }
 1338+ }
 1339+ if(!$found) {
 1340+ if(!$updated){
 1341+ $updated = true;
 1342+ $tmp_info[$key] = array('sem_act'=>1, 'name'=>$value[0][name], 'del_vals'=>array(), 'new_vals'=>array());
 1343+ }
 1344+ $tmp_info[$key][new_vals][] = array( 'plain' => $v1[value]->getWikiValue(), 'html' => $v1[value]->getShortHTMLText($this->m_linker));
 1345+ }
 1346+ }
 1347+ }
 1348+ }
 1349+ foreach($info as $key=>$value) {
 1350+ $i = SMWNotifyProcessor::getInfoFromId($key);
 1351+ if(!isset($this->m_info[$key])) {
 1352+ if($i[type]==0) {
 1353+ $tmp_info[$key] = array('sem_act' => 2, 'name'=>$value[0][name]);
 1354+ }else {
 1355+ $tmp_info[$key] = array('sem_act'=>2, 'name'=>$value[0][name], 'del_vals'=>array(), 'new_vals'=>array());
 1356+ foreach($value as $v) {
 1357+ $tmp_info[$key][new_vals][] = array( 'plain' => $v[value]->getWikiValue(), 'html' => $v[value]->getShortHTMLText($this->m_linker));
 1358+ }
 1359+ }
 1360+ }
 1361+ }
 1362+
 1363+ $notifications = $sStore->getMonitoredNotificationsDetail($page_id);
 1364+ // add semantic info to report all NM
 1365+ foreach($notifications as $user_id=>$notifies) {
 1366+ foreach($notifies['rep_all'] as $notify_id=>$notify_name) {
 1367+ foreach(array_keys($tmp_info) as $key) {
 1368+ $notifications[$user_id]['semantic'][$key][$notify_id]=$notify_name;
 1369+ }
 1370+ }
 1371+ }
 1372+ $page_name = $this->m_title->getText().' ('.$this->m_title->getFullUrl().')';
 1373+
 1374+ foreach($notifications as $user_id=>$notifies) {
 1375+ foreach($notifies['semantic'] as $key=>$notify) {
 1376+ if(isset($tmp_info[$key])) {
 1377+ $hint = "";
 1378+ if(!isset($this->m_userMsgs[$user_id])) {
 1379+ $this->m_userMsgs[$user_id] = "\r\nSemantic attributes are changed in page $page_name.";
 1380+ $hint = "Semantic attributes are changed in page <a href='".$this->m_title->getFullUrl()."'>".htmlspecialchars($this->m_title->getText())."</a>.<br/>";
 1381+ $this->m_userHtmlNMMsgs[$user_id] .= $hint;
 1382+ }
 1383+
 1384+ $this->m_userMsgs[$user_id] .= $this->getNotifyPlain($tmp_info[$key],$key).' ( NM: ';
 1385+ $propHint = $this->getNotifyHtml($tmp_info[$key],$key);
 1386+ $this->m_userHtmlPropMsgs[$user_id] .= $propHint . "<tr><td colspan='5'>Notify Me: ";
 1387+ $idx = 0;
 1388+ foreach($notify as $notify_id=>$notify_name) {
 1389+ if($idx>0) {
 1390+ $this->m_userMsgs[$user_id] .= ', ';
 1391+ $this->m_userHtmlPropMsgs[$user_id] .= ', ';
 1392+ }
 1393+ $this->m_userMsgs[$user_id] .= $notify_name;
 1394+ $this->m_userHtmlPropMsgs[$user_id] .= '<b>'.htmlspecialchars($notify_name).'</b>';
 1395+ $this->m_notifyHtmlMsgs[$notify_id] .= $hint;
 1396+ $this->m_notifyHtmlPropMsgs[$notify_id] .= $propHint;
 1397+ $idx++;
 1398+
 1399+ $this->m_userNMs[$user_id][] = $notify_id;
 1400+ }
 1401+ $this->m_userMsgs[$user_id] .= ' ).';
 1402+ $this->m_userHtmlPropMsgs[$user_id] .= "</td></tr>";
 1403+ }
 1404+ }
 1405+ }
 1406+ // get possible subquery
 1407+ $this->m_subQueryNotify = array();
 1408+ $queries = $sStore->getPossibleQuery($this->m_info);
 1409+ if(is_array($queries)) {
 1410+ foreach($queries[1] as $notify_id=>$notify) {
 1411+ $this->m_subQueryNotify[$notify_id] = $notify;
 1412+ }
 1413+ }
 1414+
 1415+ $this->m_info = $info;
 1416+ }
 1417+
 1418+ // this will cost time, think we can update monitor in a single thread, like Job
 1419+ public function updateNotifyMonitor() {
 1420+ $page_id = $this->m_title->getArticleID();
 1421+ if(($page_id == 0) || ($this->m_title->getNamespace() != NS_MAIN)) {
 1422+ return;
 1423+ }
 1424+ $sStore = NMStorage::getDatabase();
 1425+ $queries = $sStore->getPossibleQuery($this->m_info);
 1426+ if(!is_array($queries)) {
 1427+ return;
 1428+ }
 1429+ // get monitored query
 1430+ $main_queries = $sStore->getMonitoredQuery($page_id);
 1431+ foreach($queries[0] as $notify_id=>$notify) {
 1432+ $main_queries[$notify_id] = $notify;
 1433+ }
 1434+
 1435+ // begin notify query on main query
 1436+ $page_name = $this->m_title->getText().' ('.$this->m_title->getFullUrl().')';
 1437+ $page_html_name = '<a href="'.$this->m_title->getFullUrl().'">'.htmlspecialchars($this->m_title->getText()).'</a>';
 1438+
 1439+ foreach($main_queries as $notify_id=>$notify) {
 1440+ $sStore->getNotifyInMainQuery($page_id, $notify_id, $notify['sql'], $notify['hierarchy'], $match, $monitoring);
 1441+ if((!$monitoring) && $match) {
 1442+ $this->m_userMsgs[$notify['user_id']] .= "\r\nPage $page_name matches NotifyMe '$notify[name]' now.";
 1443+ $hint = "Page $page_html_name matches \"<b>".htmlspecialchars($notify[name])."</b>\" now.<br/>";
 1444+ $this->m_userHtmlNMMsgs[$notify['user_id']] .= $hint;
 1445+ $this->m_notifyHtmlMsgs[$notify_id] .= $hint;
 1446+ $this->m_newMonitor[] = array('notify_id'=>$notify_id, 'page_id'=>$page_id);
 1447+ } else if((!$match) && $monitoring) {
 1448+ $this->m_userMsgs[$notify['user_id']] .= "\r\nPage $page_name does not match NotifyMe '$notify[name]' now.";
 1449+ $hint = "Page $page_html_name does not match \"<b>".htmlspecialchars($notify[name])."</b>\" now.<br/>";
 1450+ $this->m_userHtmlNMMsgs[$notify['user_id']] .= $hint;
 1451+ $this->m_notifyHtmlMsgs[$notify_id] .= $hint;
 1452+ $this->m_removeMonitored[] = array('notify_id'=>$notify_id, 'page_id'=>$page_id);
 1453+ }
 1454+ $this->m_userNMs[$notify['user_id']][] = $notify_id;
 1455+ }
 1456+ // begin notify query on sub query, should go through all pages
 1457+ foreach($queries[1] as $notify_id=>$notify) {
 1458+ $this->m_subQueryNotify[$notify_id] = $notify;
 1459+ }
 1460+ foreach($this->m_subQueryNotify as $notify_id=>$notify) {
 1461+ $res = $sStore->getNotifyInSubquery($notify_id, $notify['sql'], $notify['hierarchy']);
 1462+
 1463+ $no_matches = array_diff($res['monitoring'], $res['match']);
 1464+ $matches = array_diff($res['match'], $res['monitoring']);
 1465+ foreach($matches as $pid) {
 1466+ $pt = $sStore->getPageTitle($pid);
 1467+ if(!$pt) {
 1468+ continue;
 1469+ }
 1470+ $t = Title::makeTitle( NS_MAIN, $pt->page_title );
 1471+ $p_name = $t->getText().' ('.$t->getFullUrl().')';
 1472+ $p_html_name = '<a href="'.$t->getFullUrl().'">'.htmlspecialchars($t->getText()).'</a>';
 1473+
 1474+ $this->m_userMsgs[$notify['user_id']] .= "\r\nSub page $page_name changed, $p_name matches NotifyMe '$notify[name]' now.";
 1475+ $hint = "Sub page $page_html_name changed, $p_html_name matches \"<b>".htmlspecialchars($notify[name])."</b>\" now.<br/>";
 1476+ $this->m_userHtmlNMMsgs[$notify['user_id']] .= $hint;
 1477+ $this->m_notifyHtmlMsgs[$notify_id] .= $hint;
 1478+ $this->m_newMonitor[] = array('notify_id'=>$notify_id, 'page_id'=>$pid);
 1479+
 1480+ $this->m_userNMs[$notify['user_id']][] = $notify_id;
 1481+ }
 1482+ foreach($no_matches as $pid) {
 1483+ $pt = $sStore->getPageTitle($pid);
 1484+ if(!$pt) {
 1485+ continue;
 1486+ }
 1487+ $t = Title::makeTitle( NS_MAIN, $pt->page_title );
 1488+ $p_name = $t->getText().' ('.$t->getFullUrl().')';
 1489+ $p_html_name = '<a href="'.$t->getFullUrl().'">'.htmlspecialchars($t->getText()).'</a>';
 1490+
 1491+ $this->m_userMsgs[$notify['user_id']] .= "\r\nSub page $page_name changed, page $p_name does not match NotifyMe '$notify[name]' now.";
 1492+ $hint = "Sub page $page_html_name changed, $p_html_name does not match \"<b>".htmlspecialchars($notify[name])."</b>\" now.<br/>";
 1493+ $this->m_userHtmlNMMsgs[$notify['user_id']] .= $hint;
 1494+ $this->m_notifyHtmlMsgs[$notify_id] .= $hint;
 1495+ $this->m_removeMonitored[] = array('notify_id'=>$notify_id, 'page_id'=>$pid);
 1496+
 1497+ $this->m_userNMs[$notify['user_id']][] = $notify_id;
 1498+ }
 1499+ }
 1500+
 1501+ $sStore->removeNotifyMonitor($this->m_removeMonitored);
 1502+ $sStore->addNotifyMonitor($this->m_newMonitor);
 1503+ }
 1504+ private function applyStyle($html) {
 1505+ $html = str_replace("class=\"smwtable\"", "style=\"background-color: #EEEEFF;\"", $html);
 1506+ $html = str_replace("<th", "<th style=\"background-color: #EEEEFF;text-align: left;\"", $html);
 1507+ $html = str_replace("<td", "<td style=\"background-color: #FFFFFF;padding: 1px;padding-left: 5px;padding-right: 5px;text-align: left;vertical-align: top;\"", $html);
 1508+ return $html;
 1509+ }
 1510+ public function notifyUsers() {
 1511+ global $wgSitename, $wgSMTP, $wgEmergencyContact, $wgEnotifyMeJob;
 1512+ $sStore = NMStorage::getDatabase();
 1513+
 1514+ $nm_send_jobs = array();
 1515+ $id = 0;
 1516+
 1517+ if(count($this->m_notifyHtmlMsgs) > 0) {
 1518+ $notifications = $sStore->getNotifyMe(array_keys($this->m_notifyHtmlMsgs));
 1519+ }
 1520+ $html_style = '';
 1521+//<style>
 1522+//table.smwtable{background-color: #EEEEFF;}
 1523+//table.smwtable th{background-color: #EEEEFF;text-align: left;}
 1524+//table.smwtable td{background-color: #FFFFFF;padding: 1px;padding-left: 5px;padding-right: 5px;text-align: left;vertical-align: top;}
 1525+//table.smwtable tr.smwfooter td{font-size: 90%;line-height: 1;background-color: #EEEEFF;padding: 0px;padding-left: 5px;padding-right: 5px;text-align: right;vertical-align: top;}
 1526+//</style>';
 1527+ $html_showall = array();
 1528+ foreach($this->m_notifyHtmlMsgs as $notify_id=>$msg) {
 1529+ $html_msg = $html_style;
 1530+ $showing_all = false;
 1531+ if(isset($notifications[$notify_id]) && $notifications[$notify_id]['show_all']) {
 1532+ SMWQueryProcessor::processFunctionParams(explode("\n", $notifications[$notify_id]['query']), $querystring, $params, $printouts);
 1533+
 1534+ $format = 'auto';
 1535+ if (array_key_exists('format', $params)) {
 1536+ $format = strtolower(trim($params['format']));
 1537+ global $smwgResultFormats;
 1538+ if ( !array_key_exists($format, $smwgResultFormats) ) {
 1539+ $format = 'auto';
 1540+ }
 1541+ }
 1542+ $query = SMWQueryProcessor::createQuery($querystring, $params, SMWQueryProcessor::INLINE_QUERY, $format, $printouts);
 1543+ $res = smwfGetStore()->getQueryResult($query);
 1544+ $printer = SMWQueryProcessor::getResultPrinter($format, SMWQueryProcessor::INLINE_QUERY, $res);
 1545+ $result = $printer->getResult($res, $params, SMW_OUTPUT_HTML);
 1546+ $html_msg .= $result . '<br/>';
 1547+ $html_showall[$notify_id] = array ('name'=>$notifications[$notify_id]['name'], 'html'=>$result);
 1548+
 1549+ $showing_all = true;
 1550+ $link = $res->getQueryLink()->getURL();
 1551+ }
 1552+ global $smwgNMHideDiffWhenShowAll;
 1553+ if(!($smwgNMHideDiffWhenShowAll && $showing_all)) {
 1554+ $html_msg .= '<p><b>Semantic changes from last revision:</b><br/><span style="font-size: 8pt;">'.$this->m_notifyHtmlMsgs[$notify_id].'</span></P>';
 1555+ if(isset($this->m_notifyHtmlPropMsgs[$notify_id])) {
 1556+ $html_msg .= "<P><table class=\"smwtable\"><tr><th>Semantic type</th><th>Name</th><th>Action</th><th>Deleted</th><th>Added</th></tr>";
 1557+ $html_msg .= $this->m_notifyHtmlPropMsgs[$notify_id];
 1558+ $html_msg .= "</table></P>";
 1559+ }
 1560+ }
 1561+ if($showing_all) {
 1562+ $id = $sStore->addNotifyRSS('nid', $notify_id, "All current items, ".date('Y-m-d H:i:s', time()), $this->applyStyle($html_msg), $link);
 1563+ } else {
 1564+ $id = $sStore->addNotifyRSS('nid', $notify_id, $this->m_title->getText(), $this->applyStyle($html_msg));
 1565+ }
 1566+ }
 1567+ foreach($this->m_userMsgs as $user_id=>$msg) {
 1568+ // generate RSS items
 1569+ $html_msg = $html_style;
 1570+ foreach(array_unique($this->m_userNMs[$user_id]) as $showall_nid) {
 1571+ if(isset($html_showall[$showall_nid])) {
 1572+ $html_msg .= "<br/>All current items for \"<b>".$html_showall[$showall_nid]['name']."</b>\":<br/>";
 1573+ $html_msg .= $html_showall[$showall_nid]['html'] . '<br/>';
 1574+ }
 1575+ }
 1576+
 1577+ $html_msg .= '<p><b>Semantic changes from last revision:</b><br/><span style="font-size: 8pt;">'.$this->m_userHtmlNMMsgs[$user_id].'</span></P>';
 1578+ if(isset($this->m_userHtmlPropMsgs[$user_id])) {
 1579+ $html_msg .= "<P><table class=\"smwtable\"><tr><th>Semantic type</th><th>Name</th><th>Action</th><th>Deleted</th><th>Added</th></tr>";
 1580+ $html_msg .= $this->m_userHtmlPropMsgs[$user_id];
 1581+ $html_msg .= "</table></P>";
 1582+ }
 1583+
 1584+ $id = $sStore->addNotifyRSS('uid', $user_id, $this->m_title->getText(), $this->applyStyle($html_msg));
 1585+
 1586+ if($wgEnotifyMeJob) {
 1587+ // send notifications by mail
 1588+ $user_info = $sStore->getUserInfo($user_id);
 1589+ if(($user_info->user_email != '') && $this->getUserNMOption($user_info->user_options)) {
 1590+ $name = (($user_info->user_real_name=='')?$user_info->user_name:$user_info->user_real_name);
 1591+ $body = "Dear Mr./Mrs. $name,\r\n$msg".
 1592+ "\r\n\r\nSincerely yours,\r\nSMW NotifyMe Bot";
 1593+
 1594+ $params = array('to' => new MailAddress($user_info->user_email, $name),
 1595+ 'from' => new MailAddress($wgEmergencyContact, 'Admin'),
 1596+ 'subj' => 'New SMW Notification comes, from '.$wgSitename,
 1597+ 'body' => $body,
 1598+ 'replyto' => new MailAddress($wgEmergencyContact, 'Admin'));
 1599+
 1600+ $nm_send_jobs[] = new SMW_NMSendMailJob($this->m_title, $params);
 1601+ }
 1602+ }
 1603+ }
 1604+
 1605+ if( $wgEnotifyMeJob ) {
 1606+ if( count( $nm_send_jobs ) ) {
 1607+ Job :: batchInsert($nm_send_jobs);
 1608+ }
 1609+ } else {
 1610+ global $phpInterpreter;
 1611+ if (!isset($phpInterpreter)) {
 1612+ // if $phpInterpreter is not set, assume it is in search path
 1613+ // if not, starting of bot will FAIL!
 1614+ $phpInterpreter = "php";
 1615+ }
 1616+ // copy from SMW_GardeningBot.php
 1617+ ob_start();
 1618+ phpinfo();
 1619+ $info = ob_get_contents();
 1620+ ob_end_clean();
 1621+ //Get Systemstring
 1622+ preg_match('!\nSystem(.*?)\n!is',strip_tags($info),$ma);
 1623+ //Check if it consists 'windows' as string
 1624+ preg_match('/[Ww]indows/',$ma[1],$os);
 1625+ global $smwgNMIP ;
 1626+ if($os[0]=='' && $os[0]==null ) {
 1627+
 1628+ //FIXME: $runCommand must allow whitespaces in paths too
 1629+ $runCommand = "$phpInterpreter -q $smwgNMIP/specials/SMWNotifyMe/SMW_NMSendMailAsync.php";
 1630+ //TODO: test async code for linux.
 1631+ //low prio
 1632+ $nullResult = `$runCommand > /dev/null &`;
 1633+ }
 1634+ else //windowze
 1635+ {
 1636+ $runCommand = "\"\"$phpInterpreter\" -q \"$smwgNMIP/specials/SMWNotifyMe/SMW_NMSendMailAsync.php\"\"";
 1637+ $wshShell = new COM("WScript.Shell");
 1638+ $runCommand = "cmd /C ".$runCommand;
 1639+
 1640+ $oExec = $wshShell->Run($runCommand, 7, false);
 1641+ }
 1642+ }
 1643+ }
 1644+ // copy from user class
 1645+ function getUserNMOption( $str ) {
 1646+ $options = array();
 1647+ $a = explode( "\n", $str );
 1648+ foreach ( $a as $s ) {
 1649+ $m = array();
 1650+ if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
 1651+ $options[$m[1]] = $m[2];
 1652+ }
 1653+ }
 1654+ return $options['enotifyme'];
 1655+ }
 1656+}
Index: trunk/extensions/SemanticNotifyMe/includes/SNM_Initialize.php
@@ -0,0 +1,294 @@
 2+<?php
 3+/*
 4+ * Created on 24.6.2009
 5+ *
 6+ * Author: dch
 7+ */
 8+if ( !defined( 'MEDIAWIKI' ) ) die;
 9+
 10+define('SMW_NM_VERSION', '0.5');
 11+
 12+$smwgNMIP = $IP . '/extensions/SemanticNotifyMe';
 13+$smwgNMScriptPath = $wgScriptPath . '/extensions/SemanticNotifyMe';
 14+
 15+global $wgExtensionFunctions;
 16+$wgExtensionFunctions[] = 'smwgNMSetupExtension';
 17+
 18+global $wgDefaultUserOptions;
 19+$wgDefaultUserOptions['enotifyme'] = 1;
 20+
 21+function smwfNMInitMessages() {
 22+ global $smwgNMMessagesInitialized;
 23+ if (isset($smwgNMMessagesInitialized)) return; // prevent double init
 24+
 25+ smwfNMInitUserMessages(); // lazy init for ajax calls
 26+
 27+ $smwgNMMessagesInitialized = true;
 28+}
 29+function smwfNMInitUserMessages() {
 30+ global $wgMessageCache, $smwgNMContLang, $wgLanguageCode;
 31+ smwfNMInitContentLanguage($wgLanguageCode);
 32+
 33+ global $smwgNMIP, $smwgNMLang;
 34+ if (!empty($smwgNMLang)) { return; }
 35+ global $wgMessageCache, $wgLang;
 36+ $smwLangClass = 'SMW_NMLanguage' . str_replace( '-', '_', ucfirst( $wgLang->getCode() ) );
 37+
 38+ if (file_exists($smwgNMIP . '/languages/'. $smwLangClass . '.php')) {
 39+ include_once( $smwgNMIP . '/languages/'. $smwLangClass . '.php' );
 40+ }
 41+ // fallback if language not supported
 42+ if ( !class_exists($smwLangClass)) {
 43+ global $smwgNMContLang;
 44+ $smwgNMLang = $smwgNMContLang;
 45+ } else {
 46+ $smwgNMLang = new $smwLangClass();
 47+ }
 48+
 49+ $wgMessageCache->addMessages($smwgNMLang->getUserMsgArray(), $wgLang->getCode());
 50+}
 51+function smwfNMInitContentLanguage($langcode) {
 52+ global $smwgNMIP, $smwgNMContLang;
 53+ if (!empty($smwgNMContLang)) { return; }
 54+
 55+ $smwContLangClass = 'SMW_NMLanguage' . str_replace( '-', '_', ucfirst( $langcode ) );
 56+
 57+ if (file_exists($smwgNMIP . '/languages/'. $smwContLangClass . '.php')) {
 58+ include_once( $smwgNMIP . '/languages/'. $smwContLangClass . '.php' );
 59+ }
 60+
 61+ // fallback if language not supported
 62+ if ( !class_exists($smwContLangClass)) {
 63+ include_once($smwgNMIP . '/languages/SMW_NMLanguageEn.php');
 64+ $smwContLangClass = 'SMW_NMLanguageEn';
 65+ }
 66+ $smwgNMContLang = new $smwContLangClass();
 67+}
 68+
 69+function smwfNMInitializeTables() {
 70+ global $smwgNMIP;
 71+ require_once( $smwgNMIP . '/includes/SMW_NMStorage.php' );
 72+ NMStorage::getDatabase()->setup(true);
 73+
 74+ return true;
 75+}
 76+
 77+function smwfNMGetAjaxMethodPrefix() {
 78+ $func_name = isset( $_POST["rs"] ) ? $_POST["rs"] : (isset( $_GET["rs"] ) ? $_GET["rs"] : NULL);
 79+ if ($func_name == NULL) return NULL;
 80+ return substr($func_name, 4, 4); // return _xx_ of smwf_xx_methodname, may return FALSE
 81+}
 82+
 83+/**
 84+ * Intializes Semantic NotifyMe Extension.
 85+ * Called from SNM during initialization.
 86+ */
 87+function smwgNMSetupExtension() {
 88+ global $smwgNMIP, $wgHooks, $wgJobClasses, $wgExtensionCredits;
 89+ global $wgAutoloadClasses, $wgSpecialPages, $wgSpecialPageGroups;
 90+
 91+ smwfNMInitMessages();
 92+
 93+
 94+ // register SMW hooks
 95+ $wgHooks['smwInitializeTables'][] = 'smwfNMInitializeTables';
 96+
 97+ $wgHooks['ArticleSaveComplete'][] = 'smwfNMSaveHook';
 98+ $wgHooks['ArticleUndelete'][] = 'smwfNMUndeleteHook';
 99+ $wgHooks['ArticleSave'][] = 'smwfNMPreSaveHook';
 100+ $wgHooks['ArticleDelete'][] = 'smwfNMPreDeleteHook';
 101+
 102+ // queryinterface hook
 103+ $wgHooks['QI_AddButtons'][] = 'smwfNMAddQIButton';
 104+
 105+ global $wgRequest, $wgContLang;
 106+
 107+ $spns_text = $wgContLang->getNsText(NS_SPECIAL);
 108+
 109+ // register AddHTMLHeader functions for special pages
 110+ // to include javascript and css files (only on special page requests).
 111+ if (stripos($wgRequest->getRequestURL(), $spns_text.":") !== false
 112+ || stripos($wgRequest->getRequestURL(), $spns_text."%3A") !== false) {
 113+
 114+ if ( defined( 'SMW_HALO_VERSION' ) ){
 115+ // insert NM header hook before add halo header
 116+ foreach($wgHooks['BeforePageDisplay'] as $k=>$hookVal) {
 117+ if($hookVal=='smwfHaloAddHTMLHeader') {
 118+ $wgHooks['BeforePageDisplay'][$k] = 'smwNMAddHTMLHeader';
 119+ break;
 120+ }
 121+ }
 122+ $wgHooks['BeforePageDisplay'][]='smwfHaloAddHTMLHeader';
 123+ } else {
 124+ $wgHooks['BeforePageDisplay'][]='smwNMAddHTMLHeader';
 125+ }
 126+
 127+ }
 128+
 129+ $wgJobClasses['SMW_NMSendMailJob'] = 'SMW_NMSendMailJob';
 130+ require_once($smwgNMIP . '/includes/jobs/SMW_NMSendMailJob.php');
 131+ $wgJobClasses['SMWNMRefreshJob'] = 'SMWNMRefreshJob';
 132+ require_once($smwgNMIP . '/includes/jobs/SMW_NMRefreshJob.php');
 133+
 134+ $wgAutoloadClasses['SMWNotifyProcessor'] = $smwgNMIP . '/includes/SMW_NotifyProcessor.php';
 135+
 136+ $action = $wgRequest->getVal('action');
 137+ // add some AJAX calls
 138+ if ($action == 'ajax') {
 139+ $method_prefix = smwfNMGetAjaxMethodPrefix();
 140+
 141+ // decide according to ajax method prefix which script(s) to import
 142+ switch($method_prefix) {
 143+ case '_nm_' :
 144+ require_once($smwgNMIP . '/specials/SMWNotifyMe/SMW_NotAjaxAccess.php');
 145+ break;
 146+ }
 147+ } else { // otherwise register special pages
 148+ $wgAutoloadClasses['SMWNotifyMe'] = $smwgNMIP . '/specials/SMWNotifyMe/SMWNotifyMe.php';
 149+ $wgSpecialPages['NotifyMe'] = array('SMWNotifyMe');
 150+ $wgSpecialPageGroups['NotifyMe'] = 'smwplus_group';
 151+ }
 152+
 153+ // Register Credits
 154+ $wgExtensionCredits['parserhook'][]= array(
 155+ 'name'=>'Semantic&nbsp;NotifyMe&nbsp;Extension', 'version'=>SMW_NM_VERSION,
 156+ 'author'=>"Ning Hu, Justin Zhang, [http://smwforum.ontoprise.com/smwforum/index.php/Jesse_Wang Jesse Wang], sponsored by [http://projecthalo.com Project Halo], [http://www.vulcan.com Vulcan Inc.]",
 157+ 'url'=>'http://wiking.vulcan.com/dev',
 158+ 'description' => 'Notify wiki user with specified queries.');
 159+
 160+ return true;
 161+}
 162+
 163+function smwfNMGetJSLanguageScripts(&$pathlng, &$userpathlng) {
 164+ global $smwgNMIP, $wgLanguageCode, $smwgNMScriptPath, $wgUser;
 165+
 166+ // content language file
 167+ $lng = '/scripts/Language/SMW_NMLanguage';
 168+ if (!empty($wgLanguageCode)) {
 169+ $lng .= ucfirst($wgLanguageCode).'.js';
 170+ if (file_exists($smwgNMIP . $lng)) {
 171+ $pathlng = $smwgNMScriptPath . $lng;
 172+ } else {
 173+ $pathlng = $smwgNMScriptPath . '/scripts/Language/SMW_NMLanguageEn.js';
 174+ }
 175+ } else {
 176+ $pathlng = $smwgNMScriptPath . '/scripts/Language/SMW_NMLanguageEn.js';
 177+ }
 178+
 179+ // user language file
 180+ $lng = '/scripts/Language/SMW_NMLanguage';
 181+ if (isset($wgUser)) {
 182+ $lng .= "User".ucfirst($wgUser->getOption('language')).'.js';
 183+ if (file_exists($smwgNMIP . $lng)) {
 184+ $userpathlng = $smwgNMScriptPath . $lng;
 185+ } else {
 186+ $userpathlng = $smwgNMScriptPath . '/scripts/Language/SMW_NMLanguageUserEn.js';
 187+ }
 188+ } else {
 189+ $userpathlng = $smwgNMScriptPath . '/scripts/Language/SMW_NMLanguageUserEn.js';
 190+ }
 191+}
 192+
 193+// NotifyMe scripts callback
 194+// includes necessary script and css files.
 195+function smwNMAddHTMLHeader(&$out){
 196+ global $wgTitle;
 197+ if ($wgTitle->getNamespace() != NS_SPECIAL) return true;
 198+
 199+ global $smwgNMScriptPath, $smwgScriptPath;
 200+ smwfNMGetJSLanguageScripts($pathlng, $userpathlng);
 201+
 202+ if ( defined( 'SMW_HALO_VERSION' ) ){
 203+ global $smwgHaloScriptPath, $smwgDeployVersion;
 204+
 205+ $jsm = SMWResourceManager::SINGLETON();
 206+
 207+ $jsm->addScriptIf($smwgHaloScriptPath . '/scripts/prototype.js', "all", -1, NS_SPECIAL.":NotifyMe");
 208+ $jsm->addScriptIf($smwgHaloScriptPath . '/scripts/Logger/smw_logger.js', "all", -1, NS_SPECIAL.":NotifyMe");
 209+ $jsm->addScriptIf($smwgScriptPath . '/skins/SMW_tooltip.js', "all", -1, NS_SPECIAL.":NotifyMe");
 210+
 211+ $jsm->addScriptIf($smwgNMScriptPath . '/scripts/Language/SMW_NMLanguage.js', "all", -1, NS_SPECIAL.":NotifyMe");
 212+ $jsm->addScriptIf($pathlng, "all", -1, NS_SPECIAL.":NotifyMe");
 213+ $jsm->addScriptIf($userpathlng, "all", -1, NS_SPECIAL.":NotifyMe");
 214+ $jsm->addScriptIf($smwgNMScriptPath . '/scripts/NotifyMe/NotifyHelper.js', "all", -1, NS_SPECIAL.":NotifyMe");
 215+
 216+ $jsm->addScriptIf($smwgNMScriptPath . '/scripts/Language/SMW_NMLanguage.js', "all", -1, NS_SPECIAL.":QueryInterface");
 217+ $jsm->addScriptIf($pathlng, "all", -1, NS_SPECIAL.":QueryInterface");
 218+ $jsm->addScriptIf($userpathlng, "all", -1, NS_SPECIAL.":QueryInterface");
 219+ $jsm->addScriptIf($smwgNMScriptPath . '/scripts/NotifyMe/NotifyHelper.js', "all", -1, NS_SPECIAL.":QueryInterface");
 220+
 221+ $jsm->addCSSIf($smwgScriptPath . '/skins/SMW_custom.css', "all", -1, NS_SPECIAL.":NotifyMe");
 222+ $jsm->addCSSIf($smwgNMScriptPath . '/skins/nm.css', "all", -1, NS_SPECIAL.":NotifyMe");
 223+ } else {
 224+ global $wgRequest;
 225+ $scripts = array();
 226+ $$css = array();
 227+
 228+ // read state
 229+ if ($wgRequest != NULL && $wgTitle != NULL) {
 230+ $action = $wgRequest->getVal("action");
 231+ // $action of NULL or '' means view mode
 232+ $action = $action == NULL || $action == '' ? "view" : $action;
 233+ $namespace = $wgTitle->getNamespace();
 234+ $page = $wgTitle->getNamespace().":".$wgTitle->getText();
 235+
 236+ } else { // if no state could be read, set default -> load all!
 237+ $action = "all";
 238+ $namespace = -1;
 239+ $page = array();
 240+ }
 241+ if(($namespace == NS_SPECIAL || $namespace == -1) && ($page == NS_SPECIAL.":NotifyMe")) {
 242+ $out->addScript('<script type="text/javascript" src="'. $smwgNMScriptPath . '/scripts/prototype.js"></script>');
 243+ $out->addScript('<script type="text/javascript" src="'. $smwgScriptPath . '/skins/SMW_tooltip.js"></script>');
 244+ $out->addScript('<script type="text/javascript" src="'. $smwgNMScriptPath . '/scripts/Language/SMW_NMLanguage.js"></script>');
 245+ $out->addScript('<script type="text/javascript" src="'. $pathlng .'"></script>');
 246+ $out->addScript('<script type="text/javascript" src="'. $userpathlng .'"></script>');
 247+ $out->addScript('<script type="text/javascript" src="'. $smwgNMScriptPath . '/scripts/NotifyMe/NotifyHelper.js"></script>');
 248+
 249+ $out->addLink(array(
 250+ 'rel' => 'stylesheet',
 251+ 'type' => 'text/css',
 252+ 'media' => 'screen, projection',
 253+ 'href' => $smwgScriptPath . '/skins/SMW_custom.css'
 254+ ));
 255+ $out->addLink(array(
 256+ 'rel' => 'stylesheet',
 257+ 'type' => 'text/css',
 258+ 'media' => 'screen, projection',
 259+ 'href' => $smwgNMScriptPath . '/skins/nm.css'
 260+ ));
 261+ }
 262+ }
 263+ return true; // do not load other scripts or CSS
 264+}
 265+
 266+
 267+function smwfNMPreSaveHook(&$article, &$user, &$text, &$summary, $minor, $watch, $sectionanchor, &$flags) {
 268+ SMWNotifyProcessor::prepareArticleSave($article->getTitle());
 269+
 270+ return true;
 271+}
 272+function smwfNMPreDeleteHook(&$article, &$user, &$reason) {
 273+ SMWNotifyProcessor::articleDelete($article->getTitle(), $reason);
 274+
 275+ return true;
 276+}
 277+function smwfNMSaveHook(&$article, &$user, &$text) {
 278+ SMWNotifyProcessor::articleSavedComplete($article->getTitle());
 279+
 280+ return true; // always return true, in order not to stop MW's hook processing!
 281+}
 282+function smwfNMUndeleteHook(&$title, $create) {
 283+ SMWNotifyProcessor::articleSavedComplete($title);
 284+ return true; // always return true, in order not to stop MW's hook processing!
 285+}
 286+function smwfNMAddQIButton(&$buttons) {
 287+ global $wgUser;
 288+ $user_id = $wgUser->getId();
 289+ $buttons = '';
 290+ if($user_id != 0) {
 291+ $buttons = '<button class="btn" onclick="notifyhelper.saveQueryToNotify()" onmouseover="this.className=\'btn btnhov\'; Tip(\'' . wfMsg('smw_qi_tt_addNotify') . '\')" onmouseout="this.className=\'btn\'">' . wfMsg('smw_qi_addNotify') . '</button>';
 292+ }
 293+ return true;
 294+}
 295+?>
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/includes/storage/SMW_SQLStore2_QueriesNM.php
@@ -0,0 +1,724 @@
 2+<?php
 3+/**
 4+ * Query answering functions for SMWSQLStore2. Separated frmo main code for readability and
 5+ * for avoiding twice the amount of code being required on every use of a simple storage function.
 6+ *
 7+ * based on SMW, SMW_SQLStore2_Queries.php, apply query parser to NotifyMe
 8+ *
 9+ * @author dch
 10+ */
 11+
 12+global $smwgIP;
 13+include_once("$smwgIP/includes/storage/SMW_SQLStore2_Queries.php");
 14+
 15+/**
 16+ * Class that implements query answering for SMWSQLStore2.
 17+ */
 18+class SMWSQLStore2QueryEngineNM {
 19+
 20+ /// Database slave to be used
 21+ protected $m_dbs; /// TODO: should temporary tables be created on the master DB?
 22+ /// Parent SMWSQLStore2
 23+ protected $m_store;
 24+ /// Query mode copied from given query, some submethods act differently when in SMWQuery::MODE_DEBUG
 25+ protected $m_qmode;
 26+ /// Array of generated query descriptions
 27+ protected $m_queries = array();
 28+ /// Array of arrays of executed queries, indexed by the temporary table names results were fed into
 29+ protected $m_querylog = array();
 30+ /**
 31+ * Array of sorting requests ("Property_name" => "ASC"/"DESC". Used during query
 32+ * processing (where these property names are searched while compiling the query
 33+ * conditions).
 34+ */
 35+ protected $m_sortkeys;
 36+ /// Cache of computed hierarchy queries for reuse, cat/prop-value string => tablename
 37+ protected $m_hierarchies = array();
 38+ /// Local collection of error strings, passed on to callers if possible.
 39+ protected $m_errors = array();
 40+
 41+ public function __construct(&$parentstore, &$dbslave) {
 42+ $this->m_store = $parentstore;
 43+ $this->m_dbs = $dbslave;
 44+ }
 45+
 46+ /**
 47+ * Refresh the concept cache for the given concept.
 48+ *
 49+ * @param $concept Title
 50+ */
 51+ public function refreshConceptCache($concept) {
 52+ global $smwgQMaxLimit, $smwgQConceptFeatures;
 53+ $cid = $this->m_store->getSMWPageID($concept->getDBKey(), SMW_NS_CONCEPT, '');
 54+ $cid_c = $this->m_store->getSMWPageID($concept->getDBKey(), SMW_NS_CONCEPT, '', false);
 55+ if ($cid != $cid_c) {
 56+ $this->m_errors[] = "Skipping redirect concept.";
 57+ return $this->m_errors;
 58+ }
 59+ $dv = end($this->m_store->getPropertyValues($concept, SMWPropertyValue::makeProperty('_CONC')));
 60+ $desctxt = ($dv!==false)?$dv->getWikiValue():false;
 61+ $this->m_errors = array();
 62+ if ($desctxt) { // concept found
 63+ $this->m_qmode = SMWQuery::MODE_INSTANCES;
 64+ $this->m_queries = array();
 65+ $this->m_hierarchies = array();
 66+ $this->m_querylog = array();
 67+ $this->m_sortkeys = array();
 68+ SMWSQLStore2Query::$qnum = 0;
 69+
 70+ // Pre-process query:
 71+ $qp = new SMWQueryParser($smwgQConceptFeatures);
 72+ $desc = $qp->getQueryDescription($desctxt);
 73+ $qid = $this->compileQueries($desc);
 74+ $this->executeQueries($this->m_queries[$qid]); // execute query tree, resolve all dependencies
 75+ $qobj = $this->m_queries[$qid];
 76+ if ($qobj->joinfield === '') {
 77+ return;
 78+ }
 79+ // Update database:
 80+ $this->m_dbs->delete('smw_conccache', array('o_id' => $cid), 'SMW::refreshConceptCache');
 81+ $this->m_dbs->query("INSERT IGNORE INTO " . $this->m_dbs->tableName('smw_conccache') .
 82+ " SELECT DISTINCT $qobj->joinfield AS s_id, $cid AS o_id FROM " .
 83+ $this->m_dbs->tableName($qobj->jointable) . " AS $qobj->alias" . $qobj->from .
 84+ ($qobj->where?" WHERE ":'') . $qobj->where . " LIMIT $smwgQMaxLimit",
 85+ 'SMW::refreshConceptCache');
 86+ $this->m_dbs->update('smw_conc2', array('cache_date' => strtotime("now"), 'cache_count' => $this->m_dbs->affectedRows()), array('s_id' => $cid), 'SMW::refreshConceptCache');
 87+ } else { // just delete old data if there is any
 88+ $this->m_dbs->delete('smw_conccache', array('o_id' => $cid), 'SMW::refreshConceptCache');
 89+ $this->m_dbs->update('smw_conc2', array('cache_date' => NULL, 'cache_count' => NULL), array('s_id' => $cid), 'SMW::refreshConceptCache');
 90+ $this->m_errors[] = "No concept description found.";
 91+ }
 92+ $this->cleanUp();
 93+ return $this->m_errors;
 94+ }
 95+
 96+ /**
 97+ * Delete the concept cache for the given concept.
 98+ *
 99+ * @param $concept Title
 100+ */
 101+ public function deleteConceptCache($concept) {
 102+ $cid = $this->m_store->getSMWPageID($concept->getDBKey(), SMW_NS_CONCEPT, '', false);
 103+ $this->m_dbs->delete('smw_conccache', array('o_id' => $cid), 'SMW::refreshConceptCache');
 104+ $this->m_dbs->update('smw_conc2', array('cache_date' => NULL, 'cache_count' => NULL), array('s_id' => $cid), 'SMW::refreshConceptCache');
 105+ }
 106+
 107+ /**
 108+ * The new SQL store's implementation of query answering.
 109+ */
 110+ public function getQueryResult(SMWQuery $query) {
 111+ if ($query->querymode == SMWQuery::MODE_NONE) { // don't query, but return something to printer
 112+ $result = new SMWQueryResult($query->getDescription()->getPrintrequests(), $query, true);
 113+ return $result;
 114+ }
 115+ $this->m_qmode = $query->querymode;
 116+ $this->m_queries = array();
 117+ $this->m_hierarchies = array();
 118+ $this->m_querylog = array();
 119+ $this->m_errors = array();
 120+ SMWSQLStore2Query::$qnum = 0;
 121+ $this->m_sortkeys = $query->sortkeys;
 122+ // manually make final root query (to retrieve namespace,title):
 123+ $rootid = SMWSQLStore2Query::$qnum;
 124+ $qobj = new SMWSQLStore2Query();
 125+ $qobj->jointable = 'smw_ids';
 126+ $qobj->joinfield = "$qobj->alias.smw_id";
 127+ // build query dependency tree:
 128+ wfProfileIn('SMWSQLStore2Queries::compileMainQuery (SMW)');
 129+ $qid = $this->compileQueries($query->getDescription()); // compile query, build query "plan"
 130+ wfProfileOut('SMWSQLStore2Queries::compileMainQuery (SMW)');
 131+ if ($qid < 0) { // no valid/supported condition; ensure that at least only proper pages are delivered
 132+ $qid = SMWSQLStore2Query::$qnum;
 133+ $q = new SMWSQLStore2Query();
 134+ $q->jointable = 'smw_ids';
 135+ $q->joinfield = "$q->alias.smw_id";
 136+ $q->where = "$q->alias.smw_iw!=" . $this->m_dbs->addQuotes(SMW_SQL2_SMWIW) . " AND $q->alias.smw_iw!=" . $this->m_dbs->addQuotes(SMW_SQL2_SMWREDIIW) . " AND $q->alias.smw_iw!=" . $this->m_dbs->addQuotes(SMW_SQL2_SMWBORDERIW) . " AND $q->alias.smw_iw!=" . $this->m_dbs->addQuotes(SMW_SQL2_SMWINTDEFIW);
 137+ $this->m_queries[$qid] = $q;
 138+ }
 139+ // append query to root:
 140+ $qobj->components = array($qid => "$qobj->alias.smw_id");
 141+ $qobj->sortfields = $this->m_queries[$qid]->sortfields;
 142+ $this->m_queries[$rootid] = $qobj;
 143+
 144+ $this->applyOrderConditions($query,$rootid); // may extend query if needed for sorting
 145+ wfProfileIn('SMWSQLStore2Queries::executeMainQuery (SMW)');
 146+ $this->executeQueries($this->m_queries[$rootid]); // execute query tree, resolve all dependencies
 147+ wfProfileOut('SMWSQLStore2Queries::executeMainQuery (SMW)');
 148+ $result = $this->getNMQueryResult($query,$rootid);
 149+
 150+ $this->cleanUp();
 151+ $query->addErrors($this->m_errors);
 152+ return $result;
 153+ }
 154+
 155+ // added by dch
 156+ protected function getNMQueryResult($query,$rootid) {
 157+ wfProfileIn('SMWSQLStore2Queries::getNMQueryResult (SMW)');
 158+ $qobj = $this->m_queries[$rootid];
 159+ if ($qobj->joinfield !== '') {
 160+ $sql = "SELECT DISTINCT $qobj->alias.smw_title AS t,$qobj->alias.smw_namespace AS ns FROM " .
 161+ $this->m_dbs->tableName($qobj->jointable) . " AS $qobj->alias" . $qobj->from .
 162+ (($qobj->where=='')?'':' WHERE ') . $qobj->where;
 163+ } else { // empty result, no query needed
 164+ wfProfileOut('SMWSQLStore2Queries::getNMQueryResult (SMW)');
 165+ return false;
 166+ }
 167+ $tmp = '';
 168+ foreach ($this->m_querylog as $table => $log) {
 169+ foreach ($log as $l => $v) {
 170+ $tmp .= ($tmp!=''?';':'')."$table:$l";
 171+ }
 172+ }
 173+ $result = array('sql'=>$sql, 'tmp_hierarchy'=>$tmp, 'page_ids'=>array());
 174+
 175+ $res = $this->m_dbs->select($this->m_dbs->tableName($qobj->jointable) . " AS $qobj->alias" . $qobj->from, "DISTINCT $qobj->alias.smw_title AS t,$qobj->alias.smw_namespace AS ns", $qobj->where, 'SMW::getQueryResult');
 176+ while ( $row = $this->m_dbs->fetchObject($res) ) {
 177+ if($row->ns != NS_MAIN) continue;
 178+
 179+ $title = Title::makeTitle($row->ns, $row->t);
 180+ $result['page_ids'][] = $title->getArticleID();
 181+ }
 182+ $this->m_dbs->freeResult($res);
 183+
 184+ wfProfileOut('SMWSQLStore2Queries::getNMQueryResult (SMW)');
 185+ return $result;
 186+ }
 187+
 188+ /**
 189+ * Create a new SMWSQLStore2Query object that can be used to obtain results for
 190+ * the given description. The result is stored in $this->m_queries using a numeric
 191+ * key that is returned as a result of the function. Returns -1 if no query was
 192+ * created.
 193+ */
 194+ protected function compileQueries(SMWDescription $description) {
 195+ $qid = SMWSQLStore2Query::$qnum;
 196+ $query = new SMWSQLStore2Query();
 197+ if ($description instanceof SMWSomeProperty) {
 198+ $this->compilePropertyCondition($query, $description->getProperty(), $description->getDescription());
 199+ if ($query->type == SMW_SQL2_NOQUERY) $qid = -1; // drop that right away
 200+ } elseif ($description instanceof SMWNamespaceDescription) { /// TODO: One instance of smw_ids on s_id always suffices (swm_id is KEY)! Doable in execution ... (PERFORMANCE)
 201+ $query->jointable = 'smw_ids';
 202+ $query->joinfield = "$query->alias.smw_id";
 203+ $query->where = "$query->alias.smw_namespace=" . $this->m_dbs->addQuotes($description->getNamespace());
 204+ } elseif ( ($description instanceof SMWConjunction) || ($description instanceof SMWDisjunction) ) {
 205+ $query->type = ($description instanceof SMWConjunction)?SMW_SQL2_CONJUNCTION:SMW_SQL2_DISJUNCTION;
 206+ foreach ($description->getDescriptions() as $subdesc) {
 207+ $sub = $this->compileQueries($subdesc);
 208+ if ($sub >= 0) {
 209+ $query->components[$sub] = true;
 210+ }
 211+ }
 212+ } elseif ($description instanceof SMWClassDescription) {
 213+ $cqid = SMWSQLStore2Query::$qnum;
 214+ $cquery = new SMWSQLStore2Query();
 215+ $cquery->type = SMW_SQL2_CLASS_HIERARCHY;
 216+ $cquery->joinfield = array();
 217+ foreach ($description->getCategories() as $cat) {
 218+ $cid = $this->m_store->getSMWPageID($cat->getDBkey(), NS_CATEGORY, $cat->getInterwiki());
 219+ if ($cid != 0) {
 220+ $cquery->joinfield[] = $cid;
 221+ }
 222+ }
 223+ if (count($cquery->joinfield) == 0) { // empty result
 224+ $query->type = SMW_SQL2_VALUE;
 225+ $query->jointable = '';
 226+ $query->joinfield = '';
 227+ } else { // instance query with dicjunction of classes (categories)
 228+ $query->jointable = 'smw_inst2';
 229+ $query->joinfield = "$query->alias.s_id";
 230+ $query->components[$cqid] = "$query->alias.o_id";
 231+ $this->m_queries[$cqid] = $cquery;
 232+ }
 233+ } elseif ($description instanceof SMWValueDescription) { // only type '_wpg' objects can appear on query level (essentially as nominal classes)
 234+ if ($description->getDatavalue()->getTypeID() == '_wpg') {
 235+ if ($description->getComparator() == SMW_CMP_EQ) {
 236+ $query->type = SMW_SQL2_VALUE;
 237+ $oid = $this->m_store->getSMWPageID($description->getDatavalue()->getDBkey(), $description->getDatavalue()->getNamespace(),$description->getDatavalue()->getInterwiki());
 238+ $query->joinfield = array($oid);
 239+ } else { // join with smw_ids needed for other comparators (apply to title string)
 240+ $query->jointable = 'smw_ids';
 241+ $query->joinfield = "$query->alias.smw_id";
 242+ $value = $description->getDatavalue()->getSortkey();
 243+ switch ($description->getComparator()) {
 244+ case SMW_CMP_LEQ: $comp = '<='; break;
 245+ case SMW_CMP_GEQ: $comp = '>='; break;
 246+ case SMW_CMP_NEQ: $comp = '!='; break;
 247+ case SMW_CMP_LIKE:
 248+ $comp = ' LIKE ';
 249+ $value = str_replace(array('%', '_', '*', '?'), array('\%', '\_', '%', '_'), $value);
 250+ break;
 251+ }
 252+ $query->where = "$query->alias.smw_sortkey$comp" . $this->m_dbs->addQuotes($value);
 253+ }
 254+ }
 255+ } elseif ($description instanceof SMWConceptDescription) { // fetch concept definition and insert it here
 256+ $cid = $this->m_store->getSMWPageID($description->getConcept()->getDBKey(), SMW_NS_CONCEPT, '');
 257+ $row = $this->m_dbs->selectRow('smw_conc2',
 258+ array('concept_txt','concept_features','concept_size','concept_depth','cache_date'),
 259+ array('s_id'=>$cid), 'SMWSQLStore2Queries::compileQueries');
 260+ if ( $row === false ) { // no description found, concept does not exist
 261+ // keep the above query object, it yields an empty result
 262+ ///TODO: announce an error here? (maybe not, since the query processor can check for
 263+ ///non-existing concept pages which is probably the main reason for finding nothing here
 264+ } else {
 265+ global $smwgQConceptCaching, $smwgQMaxSize, $smwgQMaxDepth, $smwgQFeatures, $smwgQConceptCacheLifetime;
 266+ $may_be_computed = ($smwgQConceptCaching == CONCEPT_CACHE_NONE) ||
 267+ ( ($smwgQConceptCaching == CONCEPT_CACHE_HARD) && ( (~(~($row->concept_features+0) | $smwgQFeatures)) == 0) &&
 268+ ($smwgQMaxSize >= $row->concept_size) && ($smwgQMaxDepth >= $row->concept_depth));
 269+ if ($row->cache_date &&
 270+ ($row->cache_date > (strtotime("now") - $smwgQConceptCacheLifetime*60) ||
 271+ !$may_be_computed)) { // cached concept, use cache unless it is dead and can be revived
 272+ $query->jointable = 'smw_conccache';
 273+ $query->joinfield = "$query->alias.s_id";
 274+ $query->where = "$query->alias.o_id=" . $this->m_dbs->addQuotes($cid);
 275+ } elseif ($row->concept_txt) { // parse description and process it recursively
 276+ if ($may_be_computed) {
 277+ $qp = new SMWQueryParser();
 278+ // no defaultnamespaces here; if any, these are already in the concept
 279+ $desc = $qp->getQueryDescription($row->concept_txt);
 280+ $qid = $this->compileQueries($desc);
 281+ $query = $this->m_queries[$qid];
 282+ } else {
 283+ wfLoadExtensionMessages('SemanticMediaWiki');
 284+ $this->m_errors[] = wfMsg('smw_concept_cache_miss',$description->getConcept()->getText());
 285+ }
 286+ } // else: no cache, no description (this may happen); treat like empty concept
 287+ }
 288+ } else { // (e.g. SMWThingDescription, SMWValueList is also treated elswhere)
 289+ $qid = -1; // no condition
 290+ }
 291+ if ($qid >= 0) {
 292+ $this->m_queries[$qid] = $query;
 293+ }
 294+ if ($query->type != SMW_SQL2_DISJUNCTION) { // sortkeys are killed by disjunctions (not all parts may have them), preprocessing might try to push disjunctions downwards to safe sortkey
 295+ foreach ($query->components as $cid => $field) {
 296+ $query->sortfields = array_merge($this->m_queries[$cid]->sortfields,$query->sortfields);
 297+ }
 298+ }
 299+ return $qid;
 300+ }
 301+
 302+ /**
 303+ * Modify the given query object to account for some property condition for the given property.
 304+ * The parameter $property may be a property object or an internal storage id. This is what makes
 305+ * this method useful: it can be used even with internal properties that have no MediaWiki Title.
 306+ * $typeid is set if property ids are used, since internal properties may not have a defined type.
 307+ * Some properties cannot be queried for; in this case, the query type is changed to SMW_SQL2_NOQUERY.
 308+ * Callers need to check for this.
 309+ */
 310+ protected function compilePropertyCondition(&$query, $property, SMWDescription $valuedesc, $typeid=false) {
 311+ $query->joinfield = "$query->alias.s_id";
 312+ if ($property instanceof SMWPropertyValue) {
 313+ $typeid = $property->getTypeID();
 314+ $mode = SMWSQLStore2::getStorageMode($typeid);
 315+ $pid = $this->m_store->getSMWPropertyID($property);
 316+ $sortkey = $property->getXSDValue();
 317+ if ($mode != SMW_SQL2_SUBS2) { // also make property hierarchy (though not for all properties)
 318+ $pqid = SMWSQLStore2Query::$qnum;
 319+ $pquery = new SMWSQLStore2Query();
 320+ $pquery->type = SMW_SQL2_PROP_HIERARCHY;
 321+ $pquery->joinfield = array($pid);
 322+ $query->components[$pqid] = "$query->alias.p_id";
 323+ $this->m_queries[$pqid] = $pquery;
 324+ }
 325+ } else {
 326+ $pid = $property;
 327+ $sortkey = false;
 328+ $mode = SMWSQLStore2::getStorageMode($typeid);
 329+ if ($mode != SMW_SQL2_SUBS2) { // no property hierarchy, but normal query (not for all properties)
 330+ $query->where = "$query->alias.p_id=" . $this->m_dbs->addQuotes($pid);
 331+ }
 332+ }
 333+ $mode = SMWSQLStore2::getStorageMode($typeid);
 334+ $sortfield = ''; // used if we should sort by this property
 335+ switch ($mode) {
 336+ case SMW_SQL2_RELS2: case SMW_SQL2_SUBS2: // subconditions as subqueries (compiled)
 337+ $query->jointable = ($mode==SMW_SQL2_RELS2)?'smw_rels2':'smw_subs2';
 338+ $sub = $this->compileQueries($valuedesc);
 339+ if ($sub >= 0) {
 340+ $query->components[$sub] = "$query->alias.o_id";
 341+ }
 342+ if ( $sortkey && array_key_exists($sortkey, $this->m_sortkeys) ) {
 343+ $query->from = ' INNER JOIN ' . $this->m_dbs->tableName('smw_ids') . " AS ids$query->alias ON ids$query->alias.smw_id=$query->alias.o_id";
 344+ $sortfield = "ids$query->alias.smw_title"; /// TODO: as below, smw_ids here is possibly duplicated! Can we prevent that? (PERFORMANCE)
 345+ }
 346+ break;
 347+ case SMW_SQL2_NARY2:
 348+ $query->jointable = 'smw_rels2';
 349+ if ($valuedesc instanceof SMWValueList) { // anything else is ignored!
 350+ $typevalue = $property->getTypesValue();
 351+ $typelabels = $typevalue->getTypeLabels();
 352+ reset($typelabels);
 353+ $subqid = SMWSQLStore2Query::$qnum;
 354+ $subquery = new SMWSQLStore2Query();
 355+ $subquery->type = SMW_SQL2_CONJUNCTION;
 356+ $query->components[$subqid] = "$query->alias.o_id";
 357+ $this->m_queries[$subqid] = $subquery;
 358+ for ($i=0; $i<$valuedesc->getCount(); $i++) {
 359+ $desc = $valuedesc->getDescription($i);
 360+ if ($desc !== NULL) {
 361+ $stypeid = SMWDataValueFactory::findTypeID(current($typelabels));
 362+ $valpid = $this->m_store->getSMWPageID(strval($i),SMW_NS_PROPERTY,SMW_SQL2_SMWIW);
 363+ $valqid = SMWSQLStore2Query::$qnum;
 364+ $valquery = new SMWSQLStore2Query();
 365+ $this->compilePropertyCondition($valquery, $valpid, $desc, $stypeid);
 366+ if ($valquery->type != SMW_SQL2_NOQUERY) {
 367+ $subquery->components[$valqid] = true;
 368+ $this->m_queries[$valqid] = $valquery;
 369+ }
 370+ }
 371+ next($typelabels);
 372+ }
 373+ }
 374+ break;
 375+ case SMW_SQL2_TEXT2: // no subconditions
 376+ $query->jointable = 'smw_text2';
 377+ break;
 378+ case SMW_SQL2_ATTS2: case SMW_SQL2_SPEC2: // subquery only conj/disj of values, compile to single "where"
 379+ $query->jointable = ($mode==SMW_SQL2_ATTS2)?'smw_atts2':'smw_spec2';
 380+ $aw = $this->compileAttributeWhere($valuedesc,"$query->alias");
 381+ if ($aw != '') {
 382+ $query->where .= ($query->where?' AND ':'') . $aw;
 383+ }
 384+ if ( $sortkey && array_key_exists($sortkey, $this->m_sortkeys) ) {
 385+ if ($mode==SMW_SQL2_ATTS2) {
 386+ $sortfield = "$query->alias." . (SMWDataValueFactory::newTypeIDValue($typeid)->isNumeric()?'value_num':'value_xsd');
 387+ } else {
 388+ $sortfield = "$query->alias.value_string";
 389+ }
 390+ }
 391+ break;
 392+ default: // drop this query
 393+ $query->type = SMW_SQL2_NOQUERY;
 394+ $sortfield = false;
 395+ }
 396+ if ($sortfield) {
 397+ $query->sortfields[$sortkey] = $sortfield;
 398+ }
 399+ }
 400+
 401+ /**
 402+ * Given an SMWDescription that is just a conjunction or disjunction of
 403+ * SMWValueDescription objects, create a plain WHERE condition string for it.
 404+ */
 405+ protected function compileAttributeWhere(SMWDescription $description, $jointable) {
 406+ if ($description instanceof SMWValueDescription) {
 407+ $dv = $description->getDatavalue();
 408+ if (SMWSQLStore2::getStorageMode($dv->getTypeID()) == SMW_SQL2_SPEC2) {
 409+ $value = $dv->getXSDValue();
 410+ $field = "$jointable.value_string";
 411+ } else { //should be SMW_SQL2_ATTS2
 412+ $value = $dv->isNumeric() ? $dv->getNumericValue() : $dv->getXSDValue();
 413+ $field = $dv->isNumeric() ? "$jointable.value_num" : "$jointable.value_xsd";
 414+ }
 415+ switch ($description->getComparator()) {
 416+ case SMW_CMP_LEQ: $comp = '<='; break;
 417+ case SMW_CMP_GEQ: $comp = '>='; break;
 418+ case SMW_CMP_NEQ: $comp = '!='; break;
 419+ case SMW_CMP_LIKE:
 420+ if ($dv->getTypeID() == '_str') {
 421+ $comp = ' LIKE ';
 422+ $value = str_replace(array('%', '_', '*', '?'), array('\%', '\_', '%', '_'), $value);
 423+ } else { // LIKE only supported for strings
 424+ $comp = '=';
 425+ }
 426+ break;
 427+ case SMW_CMP_EQ: default: $comp = '='; break;
 428+ }
 429+ $result = "$field$comp" . $this->m_dbs->addQuotes($value);
 430+ } elseif ( ($description instanceof SMWConjunction) || ($description instanceof SMWDisjunction) ) {
 431+ $op = ($description instanceof SMWConjunction) ? ' AND ' : ' OR ';
 432+ $result = '';
 433+ foreach ($description->getDescriptions() as $subdesc) {
 434+ $result= $result . ( $result!=''?$op:'' ) . $this->compileAttributeWhere($subdesc,$jointable);
 435+ }
 436+ $result = "($result)";
 437+ } else {
 438+ $result = '';
 439+ }
 440+ return $result;
 441+ }
 442+
 443+ /**
 444+ * Process stored queries and change store accordingly. The query obj is modified
 445+ * so that it contains non-recursive description of a select to execute for getting
 446+ * the actual result.
 447+ */
 448+ protected function executeQueries(SMWSQLStore2Query &$query) {
 449+ switch ($query->type) {
 450+ case SMW_SQL2_TABLE: // normal query with conjunctive subcondition
 451+ foreach ($query->components as $qid => $joinfield) {
 452+ $subquery = $this->m_queries[$qid];
 453+ $this->executeQueries($subquery);
 454+ if ($subquery->jointable != '') { // join with jointable.joinfield
 455+ $query->from .= ' INNER JOIN ' . $this->m_dbs->tableName($subquery->jointable) . " AS $subquery->alias ON $joinfield=" . $subquery->joinfield;
 456+ } elseif ($subquery->joinfield !== '') { // require joinfield as "value" via WHERE
 457+ $condition = '';
 458+ foreach ($subquery->joinfield as $value) {
 459+ $condition .= ($condition?' OR ':'') . "$joinfield=" . $this->m_dbs->addQuotes($value);
 460+ }
 461+ if (count($subquery->joinfield) > 1) {
 462+ $condition = "($condition)";
 463+ }
 464+ $query->where .= (($query->where == '')?'':' AND ') . $condition;
 465+ } else { // interpret empty joinfields as impossible condition (empty result)
 466+ $query->joinfield = ''; // make whole query false
 467+ $query->jointable = '';
 468+ $query->where = '';
 469+ $query->from = '';
 470+ break;
 471+ }
 472+ if ($subquery->where != '') {
 473+ $query->where .= (($query->where == '')?'':' AND ') . $subquery->where;
 474+ }
 475+ $query->from .= $subquery->from;
 476+ }
 477+ $query->components = array();
 478+ break;
 479+ case SMW_SQL2_CONJUNCTION:
 480+ // pick one subquery with jointable as anchor point ...
 481+ reset($query->components);
 482+ $key = false;
 483+ foreach ($query->components as $qkey => $qid) {
 484+ if ($this->m_queries[$qkey]->jointable != '') {
 485+ $key = $qkey;
 486+ break;
 487+ }
 488+ }
 489+ if ($key !== false) {
 490+ $result = $this->m_queries[$key];
 491+ unset($query->components[$key]);
 492+ $this->executeQueries($result); // execute it first (may change jointable and joinfield, e.g. when making temporary tables)
 493+ // ... and append to this query the remaining queries
 494+ foreach ($query->components as $qid => $joinfield) {
 495+ $result->components[$qid] = $result->joinfield;
 496+ }
 497+ $this->executeQueries($result); // second execute, now incorporating remaining conditions
 498+ } else { // only fixed values in conjunction, make a new value without joining
 499+ $key = $qkey;
 500+ $result = $this->m_queries[$key];
 501+ unset($query->components[$key]);
 502+ foreach ($query->components as $qid => $joinfield) {
 503+ if ($result->joinfield != $this->m_queries[$qid]->joinfield) {
 504+ $result->joinfield = ''; // all other values should already be ''
 505+ break;
 506+ }
 507+ }
 508+ }
 509+ $query = $result;
 510+ break;
 511+ case SMW_SQL2_DISJUNCTION:
 512+ // modified by dch, disable TEMPORARY tables
 513+// if ($this->m_qmode !== SMWQuery::MODE_DEBUG) {
 514+// $this->m_dbs->query( "CREATE TEMPORARY TABLE " . $this->m_dbs->tableName($query->alias) .
 515+// ' ( id INT UNSIGNED KEY ) TYPE=MEMORY', 'SMW::executeQueries' );
 516+// }
 517+// $this->m_querylog[$query->alias] = array();
 518+ foreach ($query->components as $qid => $joinfield) {
 519+ $subquery = $this->m_queries[$qid];
 520+ $this->executeQueries($subquery);
 521+ $sql = '';
 522+ if ($subquery->jointable != '') {
 523+// $sql = "INSERT IGNORE INTO " . $this->m_dbs->tableName($query->alias) . " SELECT $subquery->joinfield FROM " .
 524+// $this->m_dbs->tableName($subquery->jointable) . " AS $subquery->alias $subquery->from" . ($subquery->where?" WHERE $subquery->where":'');
 525+ $sql = ($sql != ''?'UNION':''). "SELECT $subquery->joinfield AS id FROM " .
 526+ $this->m_dbs->tableName($subquery->jointable) . " AS $subquery->alias $subquery->from" . ($subquery->where?" WHERE $subquery->where":'');
 527+ } elseif ($subquery->joinfield !== '') {
 528+ /// NOTE: this works only for single "unconditional" values without further
 529+ /// WHERE or FROM. The execution must take care of not creating any others.
 530+ $values = '';
 531+ foreach ($subquery->joinfield as $value) {
 532+ $values .= ($values?',':'') . '(' . $this->m_dbs->addQuotes($value) . ')';
 533+ }
 534+// $sql = "INSERT IGNORE INTO " . $this->m_dbs->tableName($query->alias) . " (id) VALUES $values";
 535+ $sql = ($sql != ''?'UNION':''). "SELECT smw_id AS id FROM ".$this->m_dbs->tableName('smw_ids')." WHERE smw_id IN ($values)";
 536+ } // else: // interpret empty joinfields as impossible condition (empty result), ignore
 537+// if ($sql) {
 538+// $this->m_querylog[$query->alias][] = $sql;
 539+// if ($this->m_qmode !== SMWQuery::MODE_DEBUG) {
 540+// $this->m_dbs->query($sql , 'SMW::executeQueries');
 541+// }
 542+// }
 543+ }
 544+// $query->jointable = $query->alias;
 545+ $query->jointable = "($sql)";
 546+ $query->joinfield = "$query->alias.id";
 547+ $query->sortfields = array(); // make sure we got no sortfields
 548+ /// TODO: currently this eliminates sortkeys, possibly keep them (needs different temp table format though, maybe not such a good thing to do)
 549+ break;
 550+ case SMW_SQL2_PROP_HIERARCHY: case SMW_SQL2_CLASS_HIERARCHY: // make a saturated hierarchy
 551+ $this->executeHierarchyQuery($query);
 552+ break;
 553+ case SMW_SQL2_VALUE: break; // nothing to do
 554+ }
 555+ }
 556+
 557+ /**
 558+ * Find subproperties or subcategories. This may require iterative computation,
 559+ * and temporary tables are used in many cases.
 560+ */
 561+ protected function executeHierarchyQuery(&$query) {
 562+ $fname = "SMWSQLStore2Queries::executeQueries-hierarchy-$query->type (SMW)";
 563+ wfProfileIn($fname);
 564+ global $smwgQSubpropertyDepth, $smwgQSubcategoryDepth;
 565+ $depth = ($query->type == SMW_SQL2_PROP_HIERARCHY)?$smwgQSubpropertyDepth:$smwgQSubcategoryDepth;
 566+ if ($depth <= 0) { // treat as value, no recursion
 567+ $query->type = SMW_SQL2_VALUE;
 568+ wfProfileOut($fname);
 569+ return;
 570+ }
 571+ $values = '';
 572+ $valuecond = '';
 573+ foreach ($query->joinfield as $value) {
 574+ $values .= ($values?',':'') . '(' . $this->m_dbs->addQuotes($value) . ')';
 575+ $valuecond .= ($valuecond?' OR ':'') . 'o_id=' . $this->m_dbs->addQuotes($value);
 576+ }
 577+ $smw_subs2 = $this->m_dbs->tableName('smw_subs2');
 578+ // try to safe time (SELECT is cheaper than creating/dropping 3 temp tables):
 579+ $res = $this->m_dbs->select($smw_subs2,'s_id',$valuecond, array('LIMIT'=>1));
 580+ if (!$this->m_dbs->fetchObject($res)) { // no subobjects, we are done!
 581+ $this->m_dbs->freeResult($res);
 582+ $query->type = SMW_SQL2_VALUE;
 583+ wfProfileOut($fname);
 584+ return;
 585+ }
 586+ $this->m_dbs->freeResult($res);
 587+ $tablename = $this->m_dbs->tableName($query->alias);
 588+ // modified by dch
 589+// $this->m_querylog[$query->alias] = array("Recursively computed hierarchy for element(s) $values.");
 590+ $this->m_querylog[$query->alias]["$depth:$values"] = "Recursively computed hierarchy for element(s) $values.";
 591+
 592+ $query->jointable = $query->alias;
 593+ $query->joinfield = "$query->alias.id";
 594+ if ($this->m_qmode == SMWQuery::MODE_DEBUG) {
 595+ wfProfileOut($fname);
 596+ return; // no real queries in debug mode
 597+ }
 598+
 599+ $this->m_dbs->query( "CREATE TEMPORARY TABLE $tablename " .
 600+ '( id INT UNSIGNED NOT NULL KEY) TYPE=MEMORY', 'SMW::executeQueries' );
 601+ if (array_key_exists($values, $this->m_hierarchies)) { // just copy known result
 602+ $this->m_dbs->query("INSERT INTO $tablename (id) SELECT id" .
 603+ ' FROM ' . $this->m_hierarchies[$values],
 604+ 'SMW::executeQueries');
 605+ wfProfileOut($fname);
 606+ return;
 607+ }
 608+
 609+ /// NOTE: we use two helper tables. One holds the results of each new iteration, one holds the
 610+ /// results of the previous iteration. One could of course do with only the above result table,
 611+ /// but then every iteration would use all elements of this table, while only the new ones
 612+ /// obtained in the previous step are relevant. So this is a performance measure.
 613+ $tmpnew = 'smw_new';
 614+ $tmpres = 'smw_res';
 615+ $this->m_dbs->query( "CREATE TEMPORARY TABLE $tmpnew " .
 616+ '( id INT UNSIGNED ) TYPE=MEMORY', 'SMW::executeQueries' );
 617+ $this->m_dbs->query( "CREATE TEMPORARY TABLE $tmpres " .
 618+ '( id INT UNSIGNED ) TYPE=MEMORY', 'SMW::executeQueries' );
 619+
 620+ $this->m_dbs->query("INSERT IGNORE INTO $tablename (id) VALUES $values", 'SMW::executeQueries');
 621+ $this->m_dbs->query("INSERT IGNORE INTO $tmpnew (id) VALUES $values", 'SMW::executeQueries');
 622+
 623+ for ($i=0; $i<$depth; $i++) {
 624+ $this->m_dbs->query("INSERT INTO $tmpres (id) SELECT s_id FROM $smw_subs2, $tmpnew WHERE o_id=id",
 625+ 'SMW::executeQueries');
 626+ if ($this->m_dbs->affectedRows() == 0) { // no change, exit loop
 627+ break;
 628+ }
 629+ $this->m_dbs->query("INSERT IGNORE INTO $tablename (id) SELECT $tmpres.id FROM $tmpres",
 630+ 'SMW::executeQueries');
 631+ if ($this->m_dbs->affectedRows() == 0) { // no change, exit loop
 632+ break;
 633+ }
 634+
 635+ $this->m_dbs->query('TRUNCATE TABLE ' . $tmpnew, 'SMW::executeQueries'); // empty "new" table
 636+ $tmpname = $tmpnew;
 637+ $tmpnew = $tmpres;
 638+ $tmpres = $tmpname;
 639+ }
 640+
 641+ $this->m_hierarchies[$values] = $tablename;
 642+ $this->m_dbs->query('DROP TEMPORARY TABLE smw_new', 'SMW::executeQueries');
 643+ $this->m_dbs->query('DROP TEMPORARY TABLE smw_res', 'SMW::executeQueries');
 644+ wfProfileOut($fname);
 645+ }
 646+
 647+ /**
 648+ * This function modifies the given query object at $qid to account for all ordering conditions
 649+ * in the SMWQuery $query. It is always required that $qid is the id of a query that joins with
 650+ * smw_ids so that the field alias.smw_title is $available for default sorting.
 651+ */
 652+ protected function applyOrderConditions($query, $qid) {
 653+ global $smwgQSortingSupport;
 654+ if ( !$smwgQSortingSupport ) {
 655+ return;
 656+ }
 657+ $qobj = $this->m_queries[$qid];
 658+ // (1) collect required extra property descriptions:
 659+ $extraproperties = array();
 660+ foreach ($this->m_sortkeys as $propkey => $order) {
 661+ if (!array_key_exists($propkey,$qobj->sortfields)) { // find missing property to sort by
 662+ if ($propkey == '') { // sort by first result column (page titles)
 663+ $qobj->sortfields[$propkey] = "$qobj->alias.smw_sortkey";
 664+ } else { // try to extend query
 665+ $extrawhere = '';
 666+ $sortprop = SMWPropertyValue::makeUserProperty($propkey);
 667+ if ($sortprop->isValid()) {
 668+ $extraproperties[] = new SMWSomeProperty($sortprop, new SMWThingDescription());
 669+ }
 670+ }
 671+ }
 672+ }
 673+ // (2) compile according conditions and hack them into $qobj:
 674+ if (count($extraproperties) > 0) {
 675+ $desc = new SMWConjunction($extraproperties);
 676+ $newqid = $this->compileQueries($desc);
 677+ $newqobj = $this->m_queries[$newqid]; // this is always an SMW_SQL2_CONJUNCTION ...
 678+ foreach ($newqobj->components as $cid => $field) { // ... so just re-wire its dependencies
 679+ $qobj->components[$cid] = $qobj->joinfield;
 680+ $qobj->sortfields = array_merge($qobj->sortfields, $this->m_queries[$cid]->sortfields);
 681+ }
 682+ $this->m_queries[$qid] = $qobj;
 683+ }
 684+ }
 685+
 686+ /**
 687+ * Get a SQL option array for the given query and preprocessed query object at given id.
 688+ */
 689+ protected function getSQLOptions($query,$rootid) {
 690+ global $smwgQSortingSupport, $smwgQRandSortingSupport;
 691+ $result = array( 'LIMIT' => $query->getLimit() + 1, 'OFFSET' => $query->getOffset() );
 692+ // build ORDER BY options using discovered sorting fields:
 693+ if ($smwgQSortingSupport) {
 694+ $qobj = $this->m_queries[$rootid];
 695+ foreach ($this->m_sortkeys as $propkey => $order) {
 696+ if (array_key_exists($propkey,$qobj->sortfields)) { // field successfully added
 697+ if (!array_key_exists('ORDER BY', $result)) {
 698+ $result['ORDER BY'] = '';
 699+ } else {
 700+ $result['ORDER BY'] .= ', ';
 701+ }
 702+ if ('RAND()' == $order){
 703+ $result['ORDER BY'] .= " $order ";
 704+ } else {
 705+ $result['ORDER BY'] .= $qobj->sortfields[$propkey] . " $order ";
 706+ }
 707+
 708+ }
 709+ }
 710+ }
 711+ return $result;
 712+ }
 713+
 714+ /**
 715+ * After querying, make sure no temporary database tables are left.
 716+ */
 717+ protected function cleanUp() {
 718+ if ($this->m_qmode !== SMWQuery::MODE_DEBUG) {
 719+ foreach ($this->m_querylog as $table => $log) {
 720+ $this->m_dbs->query("DROP TEMPORARY TABLE " . $this->m_dbs->tableName($table), 'SMW::getQueryResult');
 721+ }
 722+ }
 723+ }
 724+
 725+}
Index: trunk/extensions/SemanticNotifyMe/includes/storage/SMW_NMStorageSQL.php
@@ -0,0 +1,700 @@
 2+<?php
 3+/**
 4+ * This file provides the access to the MediaWiki SQL database tables that are
 5+ * used by the NotifyMe extension.
 6+ *
 7+ * @author dch
 8+ *
 9+ */
 10+if ( !defined( 'MEDIAWIKI' ) ) die;
 11+global $smwgNMIP;
 12+require_once $smwgNMIP . '/includes/SMW_NMDBHelper.php';
 13+
 14+/**
 15+ * This class encapsulates all methods that care about the database tables of
 16+ * the NotifyMe extension.
 17+ *
 18+ */
 19+class NMStorageSQL {
 20+
 21+ public function setup($verbose) {
 22+
 23+ $db =& wfGetDB( DB_MASTER );
 24+
 25+ SNMDBHelper::reportProgress("Setting up NotifyMe database ...\n",$verbose);
 26+
 27+ extract( $db->tableNames('smw_nm_monitor', 'smw_nm_query', 'smw_nm_relations', 'smw_nm_rss') );
 28+
 29+ // page_id, monitored page id
 30+ SNMDBHelper::setupTable($smw_nm_monitor,
 31+ array('notify_id' => 'INT(8) UNSIGNED NOT NULL',
 32+ 'page_id' => 'INT(8) UNSIGNED NOT NULL'), $db, $verbose);
 33+ SNMDBHelper::setupIndex($smw_nm_monitor, array('page_id'), $db);
 34+ SNMDBHelper::setupTable($smw_nm_query,
 35+ array('notify_id' => 'INT(8) UNSIGNED NOT NULL KEY AUTO_INCREMENT',
 36+ 'user_id' => 'INT(8) UNSIGNED NOT NULL',
 37+ 'delegate' => 'BLOB',
 38+ 'name' => 'VARCHAR(255) binary NOT NULL',
 39+ 'rep_all' => 'TINYINT(1) NOT NULL default \'1\'',
 40+ 'show_all' => 'TINYINT(1) NOT NULL default \'0\'',
 41+ 'query' => 'BLOB NOT NULL',
 42+ 'nm_sql' => 'BLOB',
 43+ 'nm_hierarchy' => 'BLOB',
 44+ 'enable' => 'TINYINT(1) NOT NULL default \'0\''), $db, $verbose);
 45+ SNMDBHelper::setupIndex($smw_nm_query, array('user_id'), $db);
 46+ // page_id, related page / property id in notify query
 47+ SNMDBHelper::setupTable($smw_nm_relations,
 48+ array('notify_id' => 'INT(8) UNSIGNED NOT NULL',
 49+ 'smw_id' => 'INT(8) UNSIGNED NOT NULL',
 50+ // 0 category, 1 instance, 2 property
 51+ 'type' => 'INT(8) UNSIGNED NOT NULL',
 52+ 'subquery' => 'INT(8) UNSIGNED NOT NULL default \'0\''), $db, $verbose);
 53+ SNMDBHelper::setupIndex($smw_nm_relations, array('smw_id', 'notify_id'), $db);
 54+ SNMDBHelper::setupTable($smw_nm_rss,
 55+ array('msg_id' => 'INT(8) UNSIGNED NOT NULL KEY AUTO_INCREMENT',
 56+ 'mailed' => 'TINYINT(1) NOT NULL default \'0\'',
 57+ 'user_id' => 'INT(8) UNSIGNED',
 58+ 'notify_id' => 'INT(8) UNSIGNED',
 59+ 'title' => 'VARCHAR(255) binary NOT NULL',
 60+ 'link' => 'BLOB',
 61+ 'notify' => 'BLOB NOT NULL',
 62+ 'timestamp' => 'VARCHAR(14) binary NOT NULL'), $db, $verbose);
 63+ SNMDBHelper::setupIndex($smw_nm_rss, array('user_id'), $db);
 64+
 65+ SNMDBHelper::reportProgress("... done!\n",$verbose);
 66+
 67+ }
 68+
 69+ public function addNotifyQuery($user_id, $querystring, $name, $rep_all, $show_all, $delegate) {
 70+ $fname = 'NotifyMe::addNotifyQuery';
 71+ wfProfileIn( $fname );
 72+
 73+ $dbw =& wfGetDB( DB_MASTER );
 74+ $notify_id = $dbw->nextSequenceValue('nmquery_notify_id_seq');
 75+ if($dbw->insert( 'smw_nm_query', array(
 76+ 'notify_id' => $notify_id,
 77+ 'user_id' => $user_id,
 78+ 'query' => $querystring,
 79+ 'name' => $name,
 80+ 'rep_all' => $rep_all,
 81+ 'show_all' => $show_all,
 82+ 'delegate' => $delegate), $fname))
 83+ {
 84+ $notify_id = $dbw->insertId();
 85+ } else {
 86+ $notify_id = 0;
 87+ }
 88+ wfProfileOut( $fname );
 89+ return $notify_id;
 90+ }
 91+ public function removeNotifyQuery($notify_ids){
 92+ $fname = 'NotifyMe::delNotify';
 93+ wfProfileIn( $fname );
 94+ $db =& wfGetDB( DB_MASTER );
 95+ $db->delete( $db->tableName('smw_nm_monitor'), array('notify_id'=>$notify_ids), $fname);
 96+ $db->delete( $db->tableName('smw_nm_relations'), array('notify_id'=>$notify_ids), $fname);
 97+ $db->delete( $db->tableName('smw_nm_query'), array('notify_id'=>$notify_ids), $fname);
 98+ wfProfileOut( $fname );
 99+ return true;
 100+ }
 101+ public function addNotifyMonitor($add_monitor){
 102+ $fname = 'NotifyMe::addNotifyMonitor';
 103+ wfProfileIn( $fname );
 104+
 105+ $db =& wfGetDB( DB_MASTER );
 106+ $res = $db->insert( 'smw_nm_monitor', $add_monitor, $fname);
 107+ wfProfileOut( $fname );
 108+ return $res;
 109+ }
 110+ public function removeNotifyMonitor($remove_monitored){
 111+ $fname = 'NotifyMe::addNotifyMonitor';
 112+ wfProfileIn( $fname );
 113+
 114+ $db =& wfGetDB( DB_MASTER );
 115+ foreach($remove_monitored as $monitor) {
 116+ $db->delete( $db->tableName('smw_nm_monitor'), array('notify_id'=>$monitor['notify_id'], 'page_id'=>$monitor['page_id']), $fname);
 117+ }
 118+ wfProfileOut( $fname );
 119+ return $res;
 120+ }
 121+ public function addNotifyRelations($notify_id, $rels, $subquery=0){
 122+ $fname = 'NotifyMe::addNotifyRelations';
 123+ wfProfileIn( $fname );
 124+
 125+ foreach($rels as $i=>$vi) {
 126+ foreach($rels as $j=>$vj) {
 127+ if($i != $j && $vi['namespace']==$vj['namespace'] && $vi['title']==$vj['title']) {
 128+ unset($rels[$i]);
 129+ break;
 130+ }
 131+ }
 132+ }
 133+
 134+ $dbw =& wfGetDB( DB_MASTER );
 135+ $relations = array();
 136+ $new_rel = false;
 137+ foreach($rels as $smw){
 138+ $res = $dbw->select( $dbw->tableName('smw_ids'),
 139+ 'smw_id',
 140+ // array( 'smw_namespace' => $smw['namespace'], 'smw_title' => $smw['title']),
 141+ 'smw_namespace='.$smw['namespace'].' AND smw_title='.$dbw->addQuotes($smw['title']),
 142+ 'NotifyMe::getRelatedSmwId');
 143+ $rel_smw_id = 0;
 144+ if($dbw->numRows( $res ) > 0) {
 145+ $rel_smw_id = $dbw->fetchObject($res)->smw_id;
 146+ $dbw->freeResult($res);
 147+ } else {
 148+ $dbw->freeResult($res);
 149+ return false;
 150+ }
 151+
 152+ if($rel_smw_id > 0) {
 153+ $new_rel = true;
 154+ $relations[] = array(
 155+ 'notify_id' => $notify_id,
 156+ 'smw_id' => $rel_smw_id,
 157+ // $type: 0 category, 1 instance, 2 property
 158+ 'type' => (($smw['namespace']==NS_CATEGORY)?0:(($smw['namespace']==SMW_NS_PROPERTY)?2:1)),
 159+ 'subquery' => $subquery);
 160+ }
 161+ }
 162+ if($new_rel) {
 163+ $dbw->insert( 'smw_nm_relations', $relations, $fname);
 164+ }
 165+ wfProfileOut( $fname );
 166+ return true;
 167+ }
 168+ public function getNotifications($user_id){
 169+ $fname = 'NotifyMe::getNotifications';
 170+ wfProfileIn( $fname );
 171+
 172+ $result = array();
 173+
 174+ $db = wfGetDB( DB_SLAVE );
 175+ $res = $db->select( $db->tableName('smw_nm_query'),
 176+ array('notify_id', 'delegate', 'name', 'query', 'enable', 'rep_all', 'show_all'),
 177+ array('user_id'=>$user_id), $fname, array('ORDER BY' => 'notify_id'));
 178+ if($db->numRows( $res ) > 0) {
 179+ while($row = $db->fetchObject($res)) {
 180+ $result[] = array('notify_id'=>$row->notify_id,
 181+ 'delegate'=>$row->delegate,
 182+ 'name'=>$row->name,
 183+ 'query'=>$row->query,
 184+ 'enable'=>$row->enable,
 185+ 'rep_all'=>$row->rep_all,
 186+ 'show_all'=>$row->show_all);
 187+ }
 188+ }
 189+ $db->freeResult($res);
 190+ wfProfileOut( $fname );
 191+ return $result;
 192+ }
 193+ public function getNotifyMe($notify_ids){
 194+ $fname = 'NotifyMe::getNotifyMe';
 195+ wfProfileIn( $fname );
 196+
 197+ $result = array();
 198+
 199+ $db = wfGetDB( DB_SLAVE );
 200+ $res = $db->select( $db->tableName('smw_nm_query'),
 201+ array('notify_id', 'name', 'query', 'show_all'),
 202+ array('notify_id'=>$notify_ids), $fname);
 203+ if($db->numRows( $res ) > 0) {
 204+ while($row = $db->fetchObject($res)) {
 205+ $result[$row->notify_id] = array('query'=>$row->query,
 206+ 'name'=>$row->name,
 207+ 'show_all'=>$row->show_all);
 208+ }
 209+ }
 210+ $db->freeResult($res);
 211+ wfProfileOut( $fname );
 212+ return $result;
 213+ }
 214+ public function getAllNotifications(){
 215+ $fname = 'NotifyMe::getAllNotifications';
 216+ wfProfileIn( $fname );
 217+
 218+ $result = array();
 219+
 220+ $db = wfGetDB( DB_SLAVE );
 221+ $res = $db->select( $db->tableName('smw_nm_query'),
 222+ array('notify_id', 'delegate', 'name', 'query', 'enable', 'rep_all', 'show_all'), '',
 223+ $fname, array('ORDER BY' => 'notify_id'));
 224+ if($db->numRows( $res ) > 0) {
 225+ while($row = $db->fetchObject($res)) {
 226+ $result[] = array('notify_id'=>$row->notify_id,
 227+ 'delegate'=>$row->delegate,
 228+ 'name'=>$row->name,
 229+ 'query'=>$row->query,
 230+ 'enable'=>$row->enable,
 231+ 'rep_all'=>$row->rep_all,
 232+ 'show_all'=>$row->show_all);
 233+ }
 234+ }
 235+ $db->freeResult($res);
 236+ wfProfileOut( $fname );
 237+ return $result;
 238+ }
 239+ public function disableNotifyState($notify_id){
 240+ $fname = 'NotifyMe::disableState';
 241+ wfProfileIn( $fname );
 242+ $db =& wfGetDB( DB_MASTER );
 243+ $db->delete( $db->tableName('smw_nm_monitor'), array('notify_id'=>$notify_id), $fname);
 244+ $db->delete( $db->tableName('smw_nm_relations'), array('notify_id'=>$notify_id), $fname);
 245+
 246+ $this->updateNotifyState($notify_id, 0);
 247+ wfProfileOut( $fname );
 248+ return true;
 249+ }
 250+ public function updateNotifyState($notify_id, $state) {
 251+ $fname = 'NotifyMe::updateNotifyState';
 252+ wfProfileIn( $fname );
 253+
 254+ $dbw =& wfGetDB( DB_MASTER );
 255+ $dbw->update( 'smw_nm_query', array('enable' => $state), array('notify_id'=>$notify_id), $fname);
 256+ wfProfileOut( $fname );
 257+ return true;
 258+ }
 259+ public function updateDelegate($notify_id, $delegate){
 260+ $fname = 'NotifyMe::updateDelegate';
 261+ wfProfileIn( $fname );
 262+ $dbw =& wfGetDB( DB_MASTER );
 263+ $dbw->update( 'smw_nm_query', array('delegate' => $delegate), array('notify_id'=>$notify_id), $fname);
 264+ wfProfileOut( $fname );
 265+ return true;
 266+ }
 267+ public function updateNotifyReportAll($notify_id, $rep_all) {
 268+ $fname = 'NotifyMe::updateNotifyReportAll';
 269+ wfProfileIn( $fname );
 270+
 271+ $dbw =& wfGetDB( DB_MASTER );
 272+ $dbw->update( 'smw_nm_query', array('rep_all' => $rep_all), array('notify_id'=>$notify_id), $fname);
 273+ wfProfileOut( $fname );
 274+ return true;
 275+ }
 276+ public function updateNotifyShowAll($notify_id, $show_all) {
 277+ $fname = 'NotifyMe::updateNotifyShowAll';
 278+ wfProfileIn( $fname );
 279+
 280+ $dbw =& wfGetDB( DB_MASTER );
 281+ $dbw->update( 'smw_nm_query', array('show_all' => $show_all), array('notify_id'=>$notify_id), $fname);
 282+ wfProfileOut( $fname );
 283+ return true;
 284+ }
 285+
 286+ public function lookupSmwId($namespace, $title) {
 287+ $fname = 'NotifyMe::lookupSmwId';
 288+ wfProfileIn( $fname );
 289+
 290+ $result = 0;
 291+ $db = wfGetDB( DB_SLAVE );
 292+ $res = $db->selectRow( 'smw_ids', 'smw_id', array( 'smw_namespace' => $namespace, 'smw_title' => $title ), $fname );
 293+
 294+ if($res) $result = $res->smw_id;
 295+ wfProfileOut( $fname );
 296+ return $result;
 297+ }
 298+ public function getMonitoredNotifications($page_id) {
 299+ $fname = 'NotifyMe::getMonitoredNotifications';
 300+ wfProfileIn( $fname );
 301+
 302+ $result = array();
 303+
 304+ $db = wfGetDB( DB_SLAVE );
 305+
 306+ $res = $db->select( array($db->tableName('smw_nm_query').' q', $db->tableName('smw_nm_monitor').' m'),
 307+ array('q.user_id', 'q.notify_id', 'q.delegate', 'q.name', 'q.rep_all'),
 308+ 'm.page_id='.$page_id.' and m.notify_id=q.notify_id', $fname);
 309+ if($db->numRows( $res ) > 0) {
 310+ while($row = $db->fetchObject($res)) {
 311+ $ds = explode(',', $row->delegate);
 312+ $delegated = false;
 313+ foreach($ds as $delegate) {
 314+ $u = User::newFromName(trim($delegate));
 315+ if($u==null) continue;
 316+ $id = $u->getId();
 317+ if($id > 0) {
 318+ $result[$id][$row->notify_id] = array('name'=>$row->name, 'rep_all'=>$row->rep_all);
 319+ $delegated = true;
 320+ }
 321+ }
 322+ if(!$delegated) {
 323+ $result[$row->user_id][$row->notify_id] = array('name'=>$row->name, 'rep_all'=>$row->rep_all);
 324+ }
 325+ }
 326+ }
 327+ $db->freeResult($res);
 328+
 329+ wfProfileOut( $fname );
 330+ return $result;
 331+ }
 332+ public function getMonitoredNotificationsDetail($page_id) {
 333+ $fname = 'NotifyMe::getMonitoredNotificationsDetail';
 334+ wfProfileIn( $fname );
 335+
 336+ $result = array();
 337+
 338+ $notifies = $this->getMonitoredNotifications($page_id);
 339+
 340+ $db = wfGetDB( DB_SLAVE );
 341+
 342+ foreach ($notifies as $user_id=>$notify) {
 343+ foreach($notify as $notify_id=>$notify_detail) {
 344+ if($notify_detail['rep_all']==1) {
 345+ $result[$user_id]['rep_all'][$notify_id] = $notify_detail['name'];
 346+ } else {
 347+ $res = $db->select( $db->tableName('smw_nm_relations'), array('smw_id', 'type'), "notify_id=$notify_id AND subquery=0", $fname);
 348+ if($db->numRows( $res ) > 0) {
 349+ while($row = $db->fetchObject($res)) {
 350+ $result[$user_id]['semantic'][SMWNotifyProcessor::toInfoId($row->type,0,$row->smw_id)][$notify_id] = $notify_detail['name'];
 351+ }
 352+ }
 353+ $db->freeResult($res);
 354+ }
 355+ }
 356+ }
 357+
 358+ wfProfileOut( $fname );
 359+ return $result;
 360+ }
 361+ public function getPossibleQuery($info) {
 362+ $cnt = count($info);
 363+ if($cnt == 0) {
 364+ return null;
 365+ }
 366+ $fname = 'NotifyMe::getPossibleQuery';
 367+ wfProfileIn( $fname );
 368+
 369+ $first = true;
 370+ foreach($info as $key=>$value) {
 371+ if($first) {
 372+ $first = false;
 373+ } else {
 374+ $cond .= "OR ";
 375+ }
 376+ $i = SMWNotifyProcessor::getInfoFromId($key);
 377+ $cond .= "(smw_id=$i[attr_id] AND type=$i[type]) ";
 378+ }
 379+
 380+ $db =& wfGetDB( DB_SLAVE );
 381+ $result = array(0=>array(), 1=>array());
 382+ extract( $db->tableNames('smw_nm_query', 'smw_nm_relations') );
 383+ $res = $db->query("SELECT q.notify_id, q.name, q.user_id, q.delegate, q.nm_sql, q.nm_hierarchy, c1.subquery FROM (".
 384+ "SELECT count(*) cnt, notify_id, subquery FROM $smw_nm_relations ".
 385+ "WHERE $cond GROUP BY notify_id, subquery".
 386+ ") c1 INNER JOIN (".
 387+ "SELECT count(*) cnt, notify_id, subquery FROM $smw_nm_relations ".
 388+ "WHERE type<>1 GROUP BY notify_id, subquery".
 389+ ") c2 ON c1.notify_id=c2.notify_id AND c1.subquery=c2.subquery AND c1.cnt=c2.cnt ".
 390+ "LEFT JOIN $smw_nm_query q ON q.notify_id = c1.notify_id WHERE c1.cnt<=$cnt", $fname);
 391+ if($db->numRows( $res ) > 0) {
 392+ while($row = $db->fetchObject($res)) {
 393+ $ds = explode(',', $row->delegate);
 394+ $delegated = false;
 395+ foreach($ds as $delegate) {
 396+ $u = User::newFromName(trim($delegate));
 397+ if($u==null) continue;
 398+ $id = $u->getId();
 399+ if($id > 0) {
 400+ $result[$row->subquery>0?1:0][$row->notify_id] = array('user_id'=>$id, 'name'=>$row->name, 'sql'=>$row->nm_sql, 'hierarchy'=>$row->nm_hierarchy);
 401+ $delegated = true;
 402+ }
 403+ }
 404+ if(!$delegated) {
 405+ $result[$row->subquery>0?1:0][$row->notify_id] = array('user_id'=>$row->user_id, 'name'=>$row->name, 'sql'=>$row->nm_sql, 'hierarchy'=>$row->nm_hierarchy);
 406+ }
 407+ }
 408+ }
 409+ $db->freeResult($res);
 410+ wfProfileOut( $fname );
 411+ return $result;
 412+ }
 413+ public function getMonitoredQuery($page_id) {
 414+ $fname = 'NotifyMe::getMonitoredQuery';
 415+ wfProfileIn( $fname );
 416+
 417+ $db =& wfGetDB( DB_SLAVE );
 418+ $res = $db->select( array($db->tableName('smw_nm_monitor').' m', $db->tableName('smw_nm_query').' q'),
 419+ array('q.notify_id, q.delegate, q.name, q.user_id, q.nm_sql, q.nm_hierarchy'),
 420+ "m.notify_id=q.notify_id AND m.page_id=$page_id", $fname);
 421+ if($db->numRows( $res ) > 0) {
 422+ while($row = $db->fetchObject($res)) {
 423+ $ds = explode(',', $row->delegate);
 424+ $delegated = false;
 425+ foreach($ds as $delegate) {
 426+ $u = User::newFromName(trim($delegate));
 427+ if($u==null) continue;
 428+ $id = $u->getId();
 429+ if($id > 0) {
 430+ $result[$row->notify_id] = array('user_id'=>$id, 'name'=>$row->name, 'sql'=>$row->nm_sql, 'hierarchy'=>$row->nm_hierarchy);
 431+ $delegated = true;
 432+ }
 433+ }
 434+ if(!$delegated) {
 435+ $result[$row->notify_id] = array('user_id'=>$row->user_id, 'name'=>$row->name, 'sql'=>$row->nm_sql, 'hierarchy'=>$row->nm_hierarchy);
 436+ }
 437+ }
 438+ }
 439+ $db->freeResult($res);
 440+ wfProfileOut( $fname );
 441+ return $result;
 442+ }
 443+ public function updateNMSql($notify_id, $sql, $tmp_hierarchy) {
 444+ $fname = 'NotifyMe::updateNMSql';
 445+ wfProfileIn( $fname );
 446+
 447+ $dbw =& wfGetDB( DB_MASTER );
 448+ $dbw->update( 'smw_nm_query', array('nm_sql' => $sql, 'nm_hierarchy' => $tmp_hierarchy), array('notify_id'=>$notify_id), $fname);
 449+ wfProfileOut( $fname );
 450+ return true;
 451+ }
 452+ public function getNotifyInMainQuery($page_id, $notify_id, $sql, $hierarchy, &$match, &$monitoring) {
 453+ $fname = 'NotifyMe::getNotifyInMainQuery';
 454+ wfProfileIn( $fname );
 455+
 456+ $db =& wfGetDB( DB_SLAVE );
 457+
 458+ $tmp_tables = array();
 459+ if($hierarchy != '') {
 460+ $_hierarchies = array();
 461+ foreach(explode(";",$hierarchy) as $attrs) {
 462+ $part = explode(":",$attrs,3);
 463+ $tablename = $db->tableName($part[0]);
 464+ $depth = intval($part[1]);
 465+ $values = $part[2];
 466+
 467+ $tmp_tables[] = $tablename;
 468+
 469+ $db->query( "CREATE TEMPORARY TABLE $tablename ( id INT UNSIGNED NOT NULL KEY) TYPE=MEMORY", 'SMW::executeQueries' );
 470+ if (array_key_exists($values, $_hierarchies)) { // just copy known result
 471+ $db->query("INSERT INTO $tablename (id) SELECT id FROM " . $_hierarchies[$values], 'SMW::executeQueries');
 472+ } else {
 473+ $tmpnew = 'smw_new';
 474+ $tmpres = 'smw_res';
 475+ $db->query( "CREATE TEMPORARY TABLE $tmpnew ( id INT UNSIGNED ) TYPE=MEMORY", 'SMW::executeQueries' );
 476+ $db->query( "CREATE TEMPORARY TABLE $tmpres ( id INT UNSIGNED ) TYPE=MEMORY", 'SMW::executeQueries' );
 477+ $db->query("INSERT IGNORE INTO $tablename (id) VALUES $values", 'SMW::executeQueries');
 478+ $db->query("INSERT IGNORE INTO $tmpnew (id) VALUES $values", 'SMW::executeQueries');
 479+ $smw_subs2 = $db->tableName('smw_subs2');
 480+ for ($i=0; $i<$depth; $i++) {
 481+ $db->query("INSERT INTO $tmpres (id) SELECT s_id FROM $smw_subs2, $tmpnew WHERE o_id=id", 'SMW::executeQueries');
 482+ if ($db->affectedRows() == 0) { // no change, exit loop
 483+ break;
 484+ }
 485+ $db->query("INSERT IGNORE INTO $tablename (id) SELECT $tmpres.id FROM $tmpres", 'SMW::executeQueries');
 486+ if ($db->affectedRows() == 0) { // no change, exit loop
 487+ break;
 488+ }
 489+ $db->query('TRUNCATE TABLE ' . $tmpnew, 'SMW::executeQueries'); // empty "new" table
 490+ $tmpname = $tmpnew;
 491+ $tmpnew = $tmpres;
 492+ $tmpres = $tmpname;
 493+ }
 494+ $_hierarchies[$values] = $tablename;
 495+ $db->query('DROP TEMPORARY TABLE smw_new', 'SMW::executeQueries');
 496+ $db->query('DROP TEMPORARY TABLE smw_res', 'SMW::executeQueries');
 497+ }
 498+ }
 499+ }
 500+
 501+ $sql = "SELECT p.page_id FROM " . $db->tableName('page') . " AS p INNER JOIN ($sql) AS s ON s.t=p.page_title AND s.ns=p.page_namespace WHERE p.page_id=$page_id";
 502+ $res = $db->query($sql, $fname);
 503+ $match = ($db->numRows( $res ) > 0);
 504+ $db->freeResult($res);
 505+
 506+ foreach ($tmp_tables as $tablename) {
 507+ $db->query("DROP TEMPORARY TABLE $tablename", 'SMW::getQueryResult');
 508+ }
 509+
 510+ $monitoring = ($db->selectRow( 'smw_nm_monitor', array( 'page_id' ),
 511+ array( 'page_id' => $page_id, 'notify_id' => $notify_id ), $fname ) != false);
 512+
 513+ wfProfileOut( $fname );
 514+ return true;
 515+ }
 516+ public function getNotifyInSubquery($notify_id, $sql, $hierarchy) {
 517+ $fname = 'NotifyMe::getNotifyInSubquery';
 518+ wfProfileIn( $fname );
 519+
 520+ $result = array('monitoring'=>array(), 'match'=>array());
 521+
 522+ $db =& wfGetDB( DB_SLAVE );
 523+
 524+ $tmp_tables = array();
 525+ if($hierarchy != '') {
 526+ $_hierarchies = array();
 527+ foreach(explode(";",$hierarchy) as $attrs) {
 528+ $part = explode(":",$attrs,3);
 529+ $tablename = $db->tableName($part[0]);
 530+ $depth = intval($part[1]);
 531+ $values = $part[2];
 532+
 533+ $tmp_tables[] = $tablename;
 534+
 535+ $db->query( "CREATE TEMPORARY TABLE $tablename ( id INT UNSIGNED NOT NULL KEY) TYPE=MEMORY", 'SMW::executeQueries' );
 536+ if (array_key_exists($values, $_hierarchies)) { // just copy known result
 537+ $db->query("INSERT INTO $tablename (id) SELECT id FROM " . $_hierarchies[$values], 'SMW::executeQueries');
 538+ } else {
 539+ $tmpnew = 'smw_new';
 540+ $tmpres = 'smw_res';
 541+ $db->query( "CREATE TEMPORARY TABLE $tmpnew ( id INT UNSIGNED ) TYPE=MEMORY", 'SMW::executeQueries' );
 542+ $db->query( "CREATE TEMPORARY TABLE $tmpres ( id INT UNSIGNED ) TYPE=MEMORY", 'SMW::executeQueries' );
 543+ $db->query("INSERT IGNORE INTO $tablename (id) VALUES $values", 'SMW::executeQueries');
 544+ $db->query("INSERT IGNORE INTO $tmpnew (id) VALUES $values", 'SMW::executeQueries');
 545+ $smw_subs2 = $db->tableName('smw_subs2');
 546+ for ($i=0; $i<$depth; $i++) {
 547+ $db->query("INSERT INTO $tmpres (id) SELECT s_id FROM $smw_subs2, $tmpnew WHERE o_id=id", 'SMW::executeQueries');
 548+ if ($db->affectedRows() == 0) { // no change, exit loop
 549+ break;
 550+ }
 551+ $db->query("INSERT IGNORE INTO $tablename (id) SELECT $tmpres.id FROM $tmpres", 'SMW::executeQueries');
 552+ if ($db->affectedRows() == 0) { // no change, exit loop
 553+ break;
 554+ }
 555+ $db->query('TRUNCATE TABLE ' . $tmpnew, 'SMW::executeQueries'); // empty "new" table
 556+ $tmpname = $tmpnew;
 557+ $tmpnew = $tmpres;
 558+ $tmpres = $tmpname;
 559+ }
 560+ $_hierarchies[$values] = $tablename;
 561+ $db->query('DROP TEMPORARY TABLE smw_new', 'SMW::executeQueries');
 562+ $db->query('DROP TEMPORARY TABLE smw_res', 'SMW::executeQueries');
 563+ }
 564+ }
 565+ }
 566+
 567+ $res = $db->query("SELECT p.page_id FROM " . $db->tableName('page') . " AS p INNER JOIN ($sql) AS s ON s.t=p.page_title AND s.ns=p.page_namespace", $fname);
 568+ if($db->numRows( $res ) > 0) {
 569+ while($row = $db->fetchObject($res)) {
 570+ $result['match'][] = $row->page_id;
 571+ }
 572+ }
 573+ $db->freeResult($res);
 574+
 575+ foreach ($tmp_tables as $tablename) {
 576+ $db->query("DROP TEMPORARY TABLE $tablename", 'SMW::getQueryResult');
 577+ }
 578+
 579+ $res = $db->select( $db->tableName('smw_nm_monitor'), array( 'page_id' ), array( 'notify_id' => $notify_id ), $fname );
 580+ if($db->numRows( $res ) > 0) {
 581+ while($row = $db->fetchObject($res)) {
 582+ $result['monitoring'][] = $row->page_id;
 583+ }
 584+ }
 585+ $db->freeResult($res);
 586+
 587+ wfProfileOut( $fname );
 588+ return $result;
 589+ }
 590+ public function getUserInfo($user_id) {
 591+ $fname = 'NotifyMe::getUserInfo';
 592+ wfProfileIn( $fname );
 593+
 594+ $db = wfGetDB( DB_SLAVE );
 595+ $result = $db->selectRow( 'user', array( 'user_name', 'user_real_name', 'user_email', 'user_options' ), array( 'user_id' => $user_id ), $fname );
 596+
 597+ wfProfileOut( $fname );
 598+ return $result;
 599+ }
 600+ public function getPageTitle($page_id) {
 601+ $db =& wfGetDB( DB_SLAVE );
 602+ return $db->selectRow('page', 'page_title', array('page_id' => $page_id));
 603+ }
 604+ public function addNotifyRSS($type, $id, $title, $msg, $link = NULL) {
 605+ $fname = 'NotifyMe::addNotifyRSS';
 606+ wfProfileIn( $fname );
 607+ $db =& wfGetDB( DB_MASTER );
 608+
 609+ $rid = $db->nextSequenceValue('nmrss_msg_id_seq');
 610+ $values = array('msg_id' => $rid,
 611+ 'title' => $title,
 612+ 'notify' => $msg,
 613+ 'timestamp' => $db->timestamp());
 614+ if($type == "uid") {
 615+ $values['user_id'] = $id;
 616+ } else {
 617+ $values['notify_id'] = $id;
 618+ }
 619+ if($link !== NULL) {
 620+ $values['link'] = $link;
 621+ }
 622+
 623+ $db->insert( 'smw_nm_rss', $values, $fname);
 624+ $id = $db->insertId();
 625+
 626+ wfProfileOut( $fname );
 627+ return $id;
 628+ }
 629+ public function getNotifyRSS($type, $id, $limit) {
 630+ $fname = 'NotifyMe::getNotifyRSS';
 631+ wfProfileIn( $fname );
 632+ $db =& wfGetDB( DB_SLAVE );
 633+ $result = array();
 634+ if($type == "uid") {
 635+ $ids = array('user_id'=>$id);
 636+ } else {
 637+ $ids = array('notify_id'=>$id);
 638+ }
 639+ $res = $db->select( $db->tableName('smw_nm_rss'), array('title', 'link', 'notify', 'timestamp'), $ids, $fname, array('ORDER BY' => 'timestamp DESC', 'LIMIT'=>$limit));
 640+ if($db->numRows( $res ) > 0) {
 641+ while($row = $db->fetchObject($res)) {
 642+ $result[] = array('title' => $row->title, 'link'=>$row->link, 'notify'=>$row->notify, 'timestamp'=>$row->timestamp);
 643+ }
 644+ }
 645+ $db->freeResult($res);
 646+ wfProfileOut( $fname );
 647+ return $result;
 648+ }
 649+ public function getUnmailedNMMessages() {
 650+ $fname = 'NotifyMe::getUnmailedNMMessages';
 651+ wfProfileIn( $fname );
 652+ $db =& wfGetDB( DB_MASTER );
 653+ $smw_nm_rss = $db->tableName('smw_nm_rss');
 654+ $result = array();
 655+ $res = $db->select( $smw_nm_rss,
 656+ array('user_id', 'title', 'notify', 'timestamp'),
 657+ array('mailed'=>'0'), $fname);
 658+ if($db->numRows( $res ) > 0) {
 659+ while($row = $db->fetchObject($res)) {
 660+ $result[] = array('user_id' => $row->user_id, 'title' => $row->title, 'notify'=>$row->notify, 'timestamp'=>$row->timestamp);
 661+ }
 662+ }
 663+ $db->freeResult($res);
 664+ $db->update($smw_nm_rss, array('mailed' => '1'), array('mailed' => '0'), $fname);
 665+ wfProfileOut( $fname );
 666+ return $result;
 667+ }
 668+
 669+ public function getGroupedUsers($groups) {
 670+ $fname = 'NotifyMe::getGroupedUsers';
 671+ wfProfileIn( $fname );
 672+ $db =& wfGetDB( DB_SLAVE );
 673+ extract( $db->tableNames('user_groups', 'user') );
 674+ $result = array();
 675+ $res = $db->query( "SELECT u.user_name FROM $user u".
 676+ ($groups ? " LEFT JOIN $user_groups g ON u.user_id = g.ug_user
 677+ WHERE g.ug_group IN ('".join("','", $groups)."')
 678+ GROUP BY user_name":""), $fname);
 679+ if($db->numRows( $res ) > 0) {
 680+ while($row = $db->fetchObject($res)) {
 681+ $result[] = $row->user_name;
 682+ }
 683+ }
 684+ $db->freeResult($res);
 685+ wfProfileOut( $fname );
 686+ return $result;
 687+ }
 688+
 689+ function getNMQueryResult(SMWQuery $query) {
 690+ wfProfileIn('SMWSQLStore2::getNMQueryResult (SMW)');
 691+ global $smwgNMIP;
 692+ include_once("$smwgNMIP/includes/storage/SMW_SQLStore2_QueriesNM.php");
 693+ $qe = new SMWSQLStore2QueryEngineNM(smwfGetStore(), wfGetDB( DB_SLAVE ));
 694+ $result = $qe->getQueryResult($query);
 695+ wfProfileOut('SMWSQLStore2::getNMQueryResult (SMW)');
 696+ return $result;
 697+ }
 698+
 699+}
 700+
 701+?>
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/includes/SMW_NMStorage.php
@@ -0,0 +1,84 @@
 2+<?php
 3+
 4+/**
 5+ * This file provides the access to the database tables that are
 6+ * used by the NotifyMe extension.
 7+ *
 8+ * @author dch
 9+ *
 10+ */
 11+if ( !defined( 'MEDIAWIKI' ) ) die;
 12+global $smwgNMIP;
 13+require_once $smwgNMIP . '/includes/SMW_NMDBHelper.php';
 14+
 15+
 16+
 17+/**
 18+ * This class encapsulates all methods that care about the database tables of
 19+ * the NotifyMe extension. It is a singleton that contains an instance
 20+ * of the actual database access object e.g. the Mediawiki SQL database.
 21+ *
 22+ */
 23+class NMStorage {
 24+
 25+ //--- Private fields---
 26+
 27+ private static $mInstance; // NMStorage: the only instance of this singleton
 28+ private static $mDatabase; // The actual database object
 29+
 30+ //--- Constructor ---
 31+
 32+ /**
 33+ * Constructor.
 34+ * Creates the object that handles the concrete database access.
 35+ *
 36+ */
 37+ private function __construct() {
 38+ if (self::$mDatabase == NULL) {
 39+ global $smwgBaseStore;
 40+ switch ($smwgBaseStore) {
 41+ case (SMW_STORE_TESTING):
 42+ trigger_error('Testing store not implemented for NotifyMe extension.');
 43+ break;
 44+ case (SMW_STORE_MWDB):
 45+ default:
 46+ global $smwgNMIP;
 47+ require_once($smwgNMIP . '/includes/storage/SMW_NMStorageSQL.php');
 48+ self::$mDatabase = new NMStorageSQL();
 49+ break;
 50+ }
 51+ }
 52+
 53+ }
 54+
 55+ //--- Public methods ---
 56+
 57+ /**
 58+ * Returns the single instance of this class.
 59+ *
 60+ * @return NMStorage
 61+ * The single instance of this class.
 62+ */
 63+ public static function getInstance() {
 64+ if (!isset(self::$mInstance)) {
 65+ $c = __CLASS__;
 66+ self::$mInstance = new $c;
 67+ }
 68+
 69+ return self::$mInstance;
 70+ }
 71+
 72+ /**
 73+ * Returns the actual database.
 74+ *
 75+ * @return object
 76+ * The object to access the database.
 77+ */
 78+ public static function getDatabase() {
 79+ self::getInstance(); // Make sure, singleton is initialized
 80+ return self::$mDatabase;
 81+ }
 82+
 83+}
 84+
 85+?>
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/includes/SMW_NMDBHelper.php
@@ -0,0 +1,304 @@
 2+<?php
 3+/*
 4+ * Created on 19.10.2008
 5+ *
 6+ * Author: dch
 7+ */
 8+
 9+
 10+
 11+class SNMDBHelper {
 12+
 13+
 14+ /**
 15+ * Make sure the table of the given name has the given fields, provided
 16+ * as an array with entries fieldname => typeparams. typeparams should be
 17+ * in a normalised form and order to match to existing values.
 18+ *
 19+ * The function returns an array that includes all columns that have been
 20+ * changed. For each such column, the array contains an entry
 21+ * columnname => action, where action is one of 'up', 'new', or 'del'
 22+ * If the table was already fine or was created completely anew, an empty
 23+ * array is returned (assuming that both cases require no action).
 24+ *
 25+ * NOTE: the function partly ignores the order in which fields are set up.
 26+ * Only if the type of some field changes will its order be adjusted explicitly.
 27+ *
 28+ * @param string $primaryKeys
 29+ * This optional string specifies the primary keys if there is more
 30+ * than one. This is a comma separated list of column names. The primary
 31+ * keys are not altered, if the table already exists.
 32+ */
 33+ public static function setupTable($table, $fields, $db, $verbose, $primaryKeys = "") {
 34+ global $wgDBname;
 35+ SNMDBHelper::reportProgress("Setting up table $table ...\n",$verbose);
 36+ if ($db->tableExists($table) === false) { // create new table
 37+ $sql = 'CREATE TABLE ' . $wgDBname . '.' . $table . ' (';
 38+ $first = true;
 39+ foreach ($fields as $name => $type) {
 40+ if ($first) {
 41+ $first = false;
 42+ } else {
 43+ $sql .= ',';
 44+ }
 45+ $sql .= $name . ' ' . $type;
 46+ }
 47+ if (!empty($primaryKeys)) {
 48+ $sql .= ", PRIMARY KEY(".$primaryKeys.")";
 49+ }
 50+ $sql .= ') TYPE=myisam';
 51+ $db->query( $sql, 'SNMDBHelper::setupTable' );
 52+ SNMDBHelper::reportProgress(" ... new table created\n",$verbose);
 53+ return array();
 54+ } else { // check table signature
 55+ SNMDBHelper::reportProgress(" ... table exists already, checking structure ...\n",$verbose);
 56+ $res = $db->query( 'DESCRIBE ' . $table, 'SNMDBHelper::setupTable' );
 57+ $curfields = array();
 58+ $result = array();
 59+ while ($row = $db->fetchObject($res)) {
 60+ $type = strtoupper($row->Type);
 61+ if (substr($type,0,8) == 'VARCHAR(') {
 62+ $type .= ' binary'; // just assume this to be the case for VARCHAR, avoid collation checks
 63+ }
 64+ if ($row->Null != 'YES') {
 65+ $type .= ' NOT NULL';
 66+ }
 67+ if ($row->Key == 'PRI') { /// FIXME: updating "KEY" is not possible, the below query will fail in this case.
 68+ $type .= ' KEY';
 69+ }
 70+ if ($row->Extra == 'auto_increment') {
 71+ $type .= ' AUTO_INCREMENT';
 72+ }
 73+ if ($row->Default != '') {
 74+ $type .= ' default \''.$row->Default.'\'';
 75+ }
 76+
 77+ $curfields[$row->Field] = $type;
 78+ }
 79+ $position = 'FIRST';
 80+ foreach ($fields as $name => $type) {
 81+ if ( !array_key_exists($name,$curfields) ) {
 82+ SNMDBHelper::reportProgress(" ... creating column $name ... ",$verbose);
 83+ $db->query("ALTER TABLE $table ADD `$name` $type $position", 'SNMDBHelper::setupTable');
 84+ $result[$name] = 'new';
 85+ SNMDBHelper::reportProgress("done \n",$verbose);
 86+ } elseif ($curfields[$name] != $type) {// && stripos("auto_increment", $type) == -1) {
 87+ SNMDBHelper::reportProgress(" ... changing type of column $name from '$curfields[$name]' to '$type' ... ",$verbose);
 88+ $db->query("ALTER TABLE $table CHANGE `$name` `$name` $type $position", 'SNMDBHelper::setupTable');
 89+ $result[$name] = 'up';
 90+ $curfields[$name] = false;
 91+ SNMDBHelper::reportProgress("done.\n",$verbose);
 92+ } else {
 93+ SNMDBHelper::reportProgress(" ... column $name is fine\n",$verbose);
 94+ $curfields[$name] = false;
 95+ }
 96+ $position = "AFTER $name";
 97+ }
 98+ foreach ($curfields as $name => $value) {
 99+ if ($value !== false) { // not encountered yet --> delete
 100+ SNMDBHelper::reportProgress(" ... deleting obsolete column $name ... ",$verbose);
 101+ $db->query("ALTER TABLE $table DROP COLUMN `$name`", 'SNMDBHelper::setupTable');
 102+ $result[$name] = 'del';
 103+ SNMDBHelper::reportProgress("done.\n",$verbose);
 104+ }
 105+ }
 106+ SNMDBHelper::reportProgress(" ... table $table set up successfully.\n",$verbose);
 107+ return $result;
 108+ }
 109+ }
 110+
 111+ /**
 112+ * Make sure that each of the column descriptions in the given array is indexed by *one* index
 113+ * in the given DB table.
 114+ */
 115+ public static function setupIndex($table, $columns, $db) {
 116+ $table = $db->tableName($table);
 117+ $res = $db->query( 'SHOW INDEX FROM ' . $table , 'SMW::SetupIndex');
 118+ if ( !$res ) {
 119+ return false;
 120+ }
 121+ $indexes = array();
 122+ while ( $row = $db->fetchObject( $res ) ) {
 123+ if (!array_key_exists($row->Key_name, $indexes)) {
 124+ $indexes[$row->Key_name] = array();
 125+ }
 126+ $indexes[$row->Key_name][$row->Seq_in_index] = $row->Column_name;
 127+ }
 128+ foreach ($indexes as $key => $index) { // clean up existing indexes
 129+ $id = array_search(implode(',', $index), $columns );
 130+ if ( $id !== false ) {
 131+ $columns[$id] = false;
 132+ } else { // duplicate or unrequired index
 133+ if($key != 'PRIMARY') {
 134+ $db->query( 'DROP INDEX ' . $key . ' ON ' . $table, 'SMW::SetupIndex');
 135+ }
 136+ }
 137+ }
 138+
 139+ foreach ($columns as $column) { // add remaining indexes
 140+ if ($column != false) {
 141+ $db->query( "ALTER TABLE $table ADD INDEX ( $column )", 'SMW::SetupIndex');
 142+ }
 143+ }
 144+ return true;
 145+ }
 146+
 147+ /**
 148+ * Print some output to indicate progress. The output message is given by
 149+ * $msg, while $verbose indicates whether or not output is desired at all.
 150+ */
 151+ public static function reportProgress($msg, $verbose) {
 152+ if (!$verbose) {
 153+ return;
 154+ }
 155+ if (ob_get_level() == 0) { // be sure to have some buffer, otherwise some PHPs complain
 156+ ob_start();
 157+ }
 158+ print $msg;
 159+ ob_flush();
 160+ flush();
 161+ }
 162+
 163+ /**
 164+ * Transform input parameters into a suitable array of SQL options.
 165+ * The parameter $valuecol defines the string name of the column to which
 166+ * sorting requests etc. are to be applied.
 167+ */
 168+ public static function getSQLOptions($requestoptions, $valuecol = NULL) {
 169+ $sql_options = array();
 170+ if ($requestoptions !== NULL) {
 171+ if (is_numeric($requestoptions->limit) && $requestoptions->limit >= 0) {
 172+ $sql_options['LIMIT'] = $requestoptions->limit;
 173+ }
 174+ if (is_numeric($requestoptions->offset) && $requestoptions->offset > 0) {
 175+ $sql_options['OFFSET'] = $requestoptions->offset;
 176+ }
 177+ if ( ($valuecol !== NULL) && ($requestoptions->sort) ) {
 178+ if (is_array($valuecol)) {
 179+ $sql_options['ORDER BY'] = $requestoptions->ascending ? mysql_real_escape_string(implode(",",$valuecol)) : mysql_real_escape_string(implode(",",$valuecol)) . ' DESC';
 180+ } else {
 181+ $sql_options['ORDER BY'] = $requestoptions->ascending ? mysql_real_escape_string($valuecol) : mysql_real_escape_string($valuecol) . ' DESC';
 182+ }
 183+ }
 184+ }
 185+ return $sql_options;
 186+ }
 187+
 188+ public static function getSQLOptionsAsString($requestoptions, $valuecol = NULL) {
 189+ $options = SNMDBHelper::getSQLOptions($requestoptions,$valuecol);
 190+ $limit = array_key_exists('LIMIT', $options) && is_numeric($options['LIMIT'])? 'LIMIT '.$options['LIMIT'] : '';
 191+ $offset = array_key_exists('OFFSET', $options) && is_numeric($options['OFFSET']) ? 'OFFSET '.$options['OFFSET'] : '';
 192+ $orderby = array_key_exists('ORDER BY', $options) ? 'ORDER BY '.$options['ORDER BY'] : '';
 193+ return $orderby.' '.$limit.' '.$offset;
 194+ }
 195+
 196+ /**
 197+ * Transform input parameters into a suitable string of additional SQL conditions.
 198+ * The parameter $valuecol defines the string name of the column to which
 199+ * value restrictions etc. are to be applied.
 200+ * @param $requestoptions object with options
 201+ * @param $valuecol name of SQL column to which conditions apply
 202+ * @param $labelcol name of SQL column to which string conditions apply, if any
 203+ */
 204+ public static function getSQLConditions($requestoptions, $valuecol, $labelcol = NULL) {
 205+ $sql_conds = '';
 206+ if ($requestoptions !== NULL) {
 207+ $db =& wfGetDB( DB_SLAVE ); // TODO: use slave?
 208+ if ($requestoptions->boundary !== NULL) { // apply value boundary
 209+ if ($requestoptions->ascending) {
 210+ if ($requestoptions->include_boundary) {
 211+ $op = ' >= ';
 212+ } else {
 213+ $op = ' > ';
 214+ }
 215+ } else {
 216+ if ($requestoptions->include_boundary) {
 217+ $op = ' <= ';
 218+ } else {
 219+ $op = ' < ';
 220+ }
 221+ }
 222+ $sql_conds .= ' AND ' . mysql_real_escape_string($valuecol) . $op . $db->addQuotes($requestoptions->boundary);
 223+ }
 224+ $operator = isset($requestoptions->disjunctiveStrings) && $requestoptions->disjunctiveStrings === true ? ' OR ' : ' AND ';
 225+ $neutral = isset($requestoptions->disjunctiveStrings) && $requestoptions->disjunctiveStrings === true ? ' FALSE ' : ' TRUE ';
 226+ if ($labelcol !== NULL) { // apply string conditions
 227+
 228+ $sql_conds .= ' AND ( ';
 229+
 230+ foreach ($requestoptions->getStringConditions() as $strcond) {
 231+ $string = str_replace(array('_', ' '), array('\_', '\_'), $strcond->string);
 232+ switch ($strcond->condition) {
 233+ case SMWStringCondition::STRCOND_PRE:
 234+ $string .= '%';
 235+ break;
 236+ case SMWStringCondition::STRCOND_POST:
 237+ $string = '%' . $string;
 238+ break;
 239+ case SMWStringCondition::STRCOND_MID:
 240+ $string = '%' . $string . '%';
 241+ break;
 242+ }
 243+ if ($requestoptions->isCaseSensitive) {
 244+ $sql_conds .= mysql_real_escape_string($labelcol) . ' LIKE ' . $db->addQuotes($string). $operator;
 245+ } else {
 246+ $sql_conds .= ' UPPER(' . mysql_real_escape_string($labelcol) . ') LIKE UPPER(' . $db->addQuotes($string).') '.$operator;
 247+ }
 248+ }
 249+ $sql_conds .= ' '.$neutral.' ) ';
 250+ }
 251+ }
 252+ return $sql_conds;
 253+ }
 254+
 255+ /**
 256+ * Returns sql conditions of $requestoptions in an Array.
 257+ * Warning! Does not support SMWAdvRequestOptions
 258+ *
 259+ * @param SMWRequestOptions $requestoptions
 260+ * @param string $valuecol
 261+ * @param string $labelcol
 262+ * @return array
 263+ */
 264+ public static function getSQLConditionsAsArray($requestoptions, $valuecol, $labelcol = NULL) {
 265+ $sql_conds = array();
 266+ if ($requestoptions !== NULL) {
 267+ $db =& wfGetDB( DB_SLAVE );
 268+ if ($requestoptions->boundary !== NULL) { // apply value boundary
 269+ if ($requestoptions->ascending) {
 270+ if ($requestoptions->include_boundary) {
 271+ $op = ' >= ';
 272+ } else {
 273+ $op = ' > ';
 274+ }
 275+ } else {
 276+ if ($requestoptions->include_boundary) {
 277+ $op = ' <= ';
 278+ } else {
 279+ $op = ' < ';
 280+ }
 281+ }
 282+ $sql_conds[] = mysql_real_escape_string($valuecol) . $op . $db->addQuotes($requestoptions->boundary);
 283+ }
 284+ if ($labelcol !== NULL) { // apply string conditions
 285+ foreach ($requestoptions->getStringConditions() as $strcond) {
 286+ $string = str_replace(array('_', ' '), array('\_', '\_'), $strcond->string);
 287+ switch ($strcond->condition) {
 288+ case SMWStringCondition::STRCOND_PRE:
 289+ $string .= '%';
 290+ break;
 291+ case SMWStringCondition::STRCOND_POST:
 292+ $string = '%' . $string;
 293+ break;
 294+ case SMWStringCondition::STRCOND_MID:
 295+ $string = '%' . $string . '%';
 296+ break;
 297+ }
 298+ $sql_conds[] = 'UPPER('.mysql_real_escape_string($labelcol) . ') LIKE UPPER(' . $db->addQuotes($string).')';
 299+ }
 300+ }
 301+ }
 302+ return $sql_conds;
 303+ }
 304+}
 305+?>
Index: trunk/extensions/SemanticNotifyMe/includes/jobs/SMW_NMRefreshJob.php
@@ -0,0 +1,41 @@
 2+<?php
 3+/**
 4+ * @author dch
 5+ */
 6+
 7+if ( !defined( 'MEDIAWIKI' ) ) {
 8+ die( "This file is part of the Semantic NotifyMe Extension. It is not a valid entry point.\n" );
 9+}
 10+global $IP;
 11+require_once( "$IP/includes/JobQueue.php" );
 12+
 13+class SMWNMRefreshJob extends Job {
 14+
 15+ function __construct(Title $title) {
 16+ parent::__construct( 'SMWNMRefreshJob', $title);
 17+ }
 18+
 19+ /**
 20+ * Run job
 21+ * @return boolean success
 22+ */
 23+ function run() {
 24+ wfProfileIn('SMWNMRefreshJob::run (SMW)');
 25+ SMWNotifyProcessor::refreshNotifyMe();
 26+ wfProfileOut('SMWNMRefreshJob::run (SMW)');
 27+ return true;
 28+ }
 29+
 30+ /**
 31+ * This actually files the job. This is prevented if the configuration of SMW
 32+ * disables jobs.
 33+ * NOTE: Any method that inserts jobs with Job::batchInsert or otherwise must
 34+ * implement this check individually. The below is not called in these cases.
 35+ */
 36+ function insert() {
 37+ global $smwgEnableUpdateJobs;
 38+ if ($smwgEnableUpdateJobs) {
 39+ parent::insert();
 40+ }
 41+ }
 42+}
Index: trunk/extensions/SemanticNotifyMe/includes/jobs/SMW_NMSendMailJob.php
@@ -0,0 +1,52 @@
 2+<?php
 3+/*
 4+ * SMW_NMSendMailJob.php
 5+ *
 6+ * This job is triggered whenever a notify-me page was saved or removed.
 7+ *
 8+ * @author dch
 9+ *
 10+ */
 11+if ( !defined( 'MEDIAWIKI' ) ) {
 12+ die( "This file is part of the Semantic NotifyMe Extension. It is not a valid entry point.\n" );
 13+}
 14+global $IP;
 15+require_once( "$IP/includes/JobQueue.php" );
 16+
 17+
 18+class SMW_NMSendMailJob extends Job {
 19+
 20+ /**
 21+ * Creates a NMSendMailJob
 22+ *
 23+ * @param Title $title
 24+ */
 25+ function __construct($title, $params) {
 26+ wfDebug(__METHOD__." ".get_class($this)." \r\n");
 27+ wfProfileIn( __METHOD__ );
 28+ parent::__construct( get_class($this), Title::newMainPage(), $params);
 29+
 30+ wfProfileOut( __METHOD__ );
 31+ }
 32+
 33+ /**
 34+ * Run a SMW_NMSendMailJob job
 35+ * @return boolean success
 36+ */
 37+ function run() {
 38+ wfDebug(__METHOD__);
 39+ wfProfileIn( __METHOD__ );
 40+
 41+ UserMailer::send( //userMailer(
 42+ $this->params['to'],
 43+ $this->params['from'],
 44+ $this->params['subj'],
 45+ $this->params['body'],
 46+ $this->params['replyto']
 47+ );
 48+
 49+ wfProfileOut( __METHOD__ );
 50+ return true;
 51+ }
 52+}
 53+?>
Index: trunk/extensions/SemanticNotifyMe/languages/SMW_NMLanguage.php
@@ -0,0 +1,138 @@
 2+<?php
 3+/**
 4+ * @author dch
 5+ */
 6+
 7+/**
 8+ * Base class for all language classes.
 9+ */
 10+abstract class SMW_NMLanguage {
 11+
 12+ // the message arrays ...
 13+ protected $smwContentMessages;
 14+ protected $smwUserMessages;
 15+ protected $smwDatatypeLabels;
 16+ protected $smwSpecialProperties;
 17+ protected $smwSpecialSchemaProperties;
 18+ protected $smwNMDatatypes;
 19+ protected $smwNMNamespaces;
 20+ protected $smwNMNamespaceAliases;
 21+
 22+ /**
 23+ * Function that returns an array of namespace identifiers. This function
 24+ * is obsolete
 25+ */
 26+ abstract function getNamespaceArray();
 27+
 28+
 29+ /**
 30+ * Find the internal message id of some localised message string
 31+ * for a datatype. If no type of the given name exists (maybe a
 32+ * custom of compound type) then FALSE is returned.
 33+ */
 34+ function findDatatypeMsgID($label) {
 35+ return array_search($label, $this->smwDatatypeLabels);
 36+ }
 37+
 38+ /**
 39+ * Registers all special properties of this extension in Semantic Media Wiki.
 40+ *
 41+ * The language files of the NM extension contain a mapping from special
 42+ * property constants to their string representation. These mappings are
 43+ * added to the mapping defined by Semantic Media Wiki.
 44+ */
 45+ function registerSpecialProperties() {
 46+ global $smwgContLang;
 47+ foreach ($this->smwSpecialProperties as $key => $prop) {
 48+ list($typeid, $label) = $prop;
 49+ SMWPropertyValue::registerProperty($key, $typeid, $label, true);
 50+
 51+ }
 52+ }
 53+
 54+ /**
 55+ * Returns the label of the special property with the ID $propID.
 56+ * @param int propID
 57+ * ID of the special property
 58+ * @return String Label of the special property
 59+ */
 60+ function getSpecialPropertyLabel($propID) {
 61+ return $this->smwSpecialProperties[$propID];
 62+ }
 63+
 64+ /**
 65+ * Returns all labels of the special properties.
 66+ * @return array<String> Labels of the special properties
 67+ */
 68+ function getSpecialPropertyLabels() {
 69+ return $this->smwSpecialProperties;
 70+ }
 71+
 72+ function getSpecialSchemaPropertyArray() {
 73+ return $this->smwSpecialSchemaProperties;
 74+ }
 75+
 76+ function getSpecialCategoryArray() {
 77+ return $this->smwSpecialCategories;
 78+ }
 79+
 80+ function getNMDatatype($datatypeID) {
 81+ return $this->smwNMDatatypes[$datatypeID];
 82+ }
 83+
 84+ /**
 85+ * Function that returns all content messages (those that are stored
 86+ * in some article, and can thus not be translated to individual users).
 87+ */
 88+ function getContentMsgArray() {
 89+ return $this->smwContentMessages;
 90+ }
 91+
 92+ /**
 93+ * Function that returns all user messages (those that are given only to
 94+ * the current user, and can thus be given in the individual user language).
 95+ */
 96+ function getUserMsgArray() {
 97+ return $this->smwUserMessages;
 98+ }
 99+
 100+ /**
 101+ * Returns the name of the namespace with the ID <$namespaceID>.
 102+ *
 103+ * @param int $namespaceID
 104+ * ID of the namespace whose name is requested
 105+ * @return string
 106+ * Name of the namespace or <null>.
 107+ *
 108+ */
 109+ public function getNamespace($namespaceID) {
 110+ return $this->smwNMNamespaces[$namespaceID];
 111+ }
 112+
 113+ /**
 114+ * Returns the array with all namespaces of the NM extension.
 115+ *
 116+ * @return string
 117+ * Array of additional namespaces.
 118+ *
 119+ */
 120+ public function getNamespaces() {
 121+ return $this->smwNMNamespaces;
 122+ }
 123+
 124+ /**
 125+ * Returns the array with all namespace aliases of the NM extension.
 126+ *
 127+ * @return string
 128+ * Array of additional namespace aliases.
 129+ *
 130+ */
 131+ public function getNamespaceAliases() {
 132+ return $this->smwNMNamespaceAliases;
 133+ }
 134+
 135+
 136+
 137+}
 138+
 139+
Index: trunk/extensions/SemanticNotifyMe/languages/SMW_NMLanguageEn.php
@@ -0,0 +1,59 @@
 2+<?php
 3+/**
 4+ * @author dch
 5+ */
 6+
 7+global $smwgNMIP;
 8+include_once($smwgNMIP . '/languages/SMW_NMLanguage.php');
 9+
 10+class SMW_NMLanguageEn extends SMW_NMLanguage {
 11+
 12+ protected $smwContentMessages = array(
 13+
 14+ );
 15+
 16+
 17+ protected $smwUserMessages = array(
 18+ /*Messages for Notify Me*/
 19+ 'notifyme' => 'Notify Me',
 20+ 'smw_notifyme' => 'Notify Me',
 21+
 22+ 'smw_qi_addNotify' => 'Notify me',
 23+ 'smw_qi_tt_addNotify' => 'Notify me when article-updates meet query condition',
 24+ 'smw_nm_tt_query' => 'Add #ask query to Notify Me',
 25+ 'smw_nm_tt_qtext' => 'Support query in {{#ask syntax',
 26+ 'smw_nm_tt_nmm' => 'Notify Me Manager enable you to control your notifications',
 27+ 'smw_nm_tt_clipboard' => 'Copies your RSS Feed URL to the clipboard so it can easily be inserted into any RSS reader',
 28+ );
 29+
 30+
 31+ protected $smwSpecialProperties = array(
 32+ );
 33+
 34+
 35+ var $smwSpecialSchemaProperties = array (
 36+ );
 37+
 38+ var $smwSpecialCategories = array (
 39+ );
 40+
 41+ var $smwNMDatatypes = array(
 42+ );
 43+
 44+ protected $smwNMNamespaces = array(
 45+ );
 46+
 47+ protected $smwNMNamespaceAliases = array(
 48+ );
 49+
 50+ /**
 51+ * Function that returns the namespace identifiers. This is probably obsolete!
 52+ */
 53+ public function getNamespaceArray() {
 54+ return array();
 55+ }
 56+
 57+
 58+}
 59+
 60+
Index: trunk/extensions/SemanticNotifyMe/scripts/prototype.js
@@ -0,0 +1,4185 @@
 2+/* Prototype JavaScript framework, version 1.6.0
 3+ * (c) 2005-2007 Sam Stephenson
 4+ *
 5+ * Prototype is freely distributable under the terms of an MIT-style license.
 6+ * For details, see the Prototype web site: http://www.prototypejs.org/
 7+ *
 8+ *--------------------------------------------------------------------------*/
 9+
 10+var Prototype = {
 11+ Version: '1.6.0',
 12+
 13+ Browser: {
 14+ IE: !!(window.attachEvent && !window.opera),
 15+ Opera: !!window.opera,
 16+ WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
 17+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
 18+ MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
 19+ },
 20+
 21+ BrowserFeatures: {
 22+ XPath: !!document.evaluate,
 23+ ElementExtensions: !!window.HTMLElement,
 24+ SpecificElementExtensions:
 25+ document.createElement('div').__proto__ &&
 26+ document.createElement('div').__proto__ !==
 27+ document.createElement('form').__proto__
 28+ },
 29+
 30+ ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
 31+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
 32+
 33+ emptyFunction: function() { },
 34+ K: function(x) { return x }
 35+};
 36+
 37+if (Prototype.Browser.MobileSafari)
 38+ Prototype.BrowserFeatures.SpecificElementExtensions = false;
 39+
 40+if (Prototype.Browser.WebKit)
 41+ Prototype.BrowserFeatures.XPath = false;
 42+
 43+/* Based on Alex Arnell's inheritance implementation. */
 44+var Class = {
 45+ create: function() {
 46+ var parent = null, properties = $A(arguments);
 47+ if (Object.isFunction(properties[0]))
 48+ parent = properties.shift();
 49+
 50+ function klass() {
 51+ this.initialize.apply(this, arguments);
 52+ }
 53+
 54+ Object.extend(klass, Class.Methods);
 55+ klass.superclass = parent;
 56+ klass.subclasses = [];
 57+
 58+ if (parent) {
 59+ var subclass = function() { };
 60+ subclass.prototype = parent.prototype;
 61+ klass.prototype = new subclass;
 62+ parent.subclasses.push(klass);
 63+ }
 64+
 65+ for (var i = 0; i < properties.length; i++)
 66+ klass.addMethods(properties[i]);
 67+
 68+ if (!klass.prototype.initialize)
 69+ klass.prototype.initialize = Prototype.emptyFunction;
 70+
 71+ klass.prototype.constructor = klass;
 72+
 73+ return klass;
 74+ }
 75+};
 76+
 77+Class.Methods = {
 78+ addMethods: function(source) {
 79+ var ancestor = this.superclass && this.superclass.prototype;
 80+ var properties = Object.keys(source);
 81+
 82+ if (!Object.keys({ toString: true }).length)
 83+ properties.push("toString", "valueOf");
 84+
 85+ for (var i = 0, length = properties.length; i < length; i++) {
 86+ var property = properties[i], value = source[property];
 87+ if (ancestor && Object.isFunction(value) &&
 88+ value.argumentNames().first() == "$super") {
 89+ var method = value, value = Object.extend((function(m) {
 90+ return function() { return ancestor[m].apply(this, arguments) };
 91+ })(property).wrap(method), {
 92+ valueOf: function() { return method },
 93+ toString: function() { return method.toString() }
 94+ });
 95+ }
 96+ this.prototype[property] = value;
 97+ }
 98+
 99+ return this;
 100+ }
 101+};
 102+
 103+var Abstract = { };
 104+
 105+Object.extend = function(destination, source) {
 106+ for (var property in source)
 107+ destination[property] = source[property];
 108+ return destination;
 109+};
 110+
 111+Object.extend(Object, {
 112+ inspect: function(object) {
 113+ try {
 114+ if (object === undefined) return 'undefined';
 115+ if (object === null) return 'null';
 116+ return object.inspect ? object.inspect() : object.toString();
 117+ } catch (e) {
 118+ if (e instanceof RangeError) return '...';
 119+ throw e;
 120+ }
 121+ },
 122+
 123+ toJSON: function(object) {
 124+ var type = typeof object;
 125+ switch (type) {
 126+ case 'undefined':
 127+ case 'function':
 128+ case 'unknown': return;
 129+ case 'boolean': return object.toString();
 130+ }
 131+
 132+ if (object === null) return 'null';
 133+ if (object.toJSON) return object.toJSON();
 134+ if (Object.isElement(object)) return;
 135+
 136+ var results = [];
 137+ for (var property in object) {
 138+ var value = Object.toJSON(object[property]);
 139+ if (value !== undefined)
 140+ results.push(property.toJSON() + ': ' + value);
 141+ }
 142+
 143+ return '{' + results.join(', ') + '}';
 144+ },
 145+
 146+ toQueryString: function(object) {
 147+ return $H(object).toQueryString();
 148+ },
 149+
 150+ toHTML: function(object) {
 151+ return object && object.toHTML ? object.toHTML() : String.interpret(object);
 152+ },
 153+
 154+ keys: function(object) {
 155+ var keys = [];
 156+ for (var property in object)
 157+ keys.push(property);
 158+ return keys;
 159+ },
 160+
 161+ values: function(object) {
 162+ var values = [];
 163+ for (var property in object)
 164+ values.push(object[property]);
 165+ return values;
 166+ },
 167+
 168+ clone: function(object) {
 169+ return Object.extend({ }, object);
 170+ },
 171+
 172+ isElement: function(object) {
 173+ return object && object.nodeType == 1;
 174+ },
 175+
 176+ isArray: function(object) {
 177+ return object && object.constructor === Array;
 178+ },
 179+
 180+ isHash: function(object) {
 181+ return object instanceof Hash;
 182+ },
 183+
 184+ isFunction: function(object) {
 185+ return typeof object == "function";
 186+ },
 187+
 188+ isString: function(object) {
 189+ return typeof object == "string";
 190+ },
 191+
 192+ isNumber: function(object) {
 193+ return typeof object == "number";
 194+ },
 195+
 196+ isUndefined: function(object) {
 197+ return typeof object == "undefined";
 198+ }
 199+});
 200+
 201+Object.extend(Function.prototype, {
 202+ argumentNames: function() {
 203+ var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
 204+ return names.length == 1 && !names[0] ? [] : names;
 205+ },
 206+
 207+ bind: function() {
 208+ if (arguments.length < 2 && arguments[0] === undefined) return this;
 209+ var __method = this, args = $A(arguments), object = args.shift();
 210+ return function() {
 211+ return __method.apply(object, args.concat($A(arguments)));
 212+ }
 213+ },
 214+
 215+ bindAsEventListener: function() {
 216+ var __method = this, args = $A(arguments), object = args.shift();
 217+ return function(event) {
 218+ return __method.apply(object, [event || window.event].concat(args));
 219+ }
 220+ },
 221+
 222+ curry: function() {
 223+ if (!arguments.length) return this;
 224+ var __method = this, args = $A(arguments);
 225+ return function() {
 226+ return __method.apply(this, args.concat($A(arguments)));
 227+ }
 228+ },
 229+
 230+ delay: function() {
 231+ var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
 232+ return window.setTimeout(function() {
 233+ return __method.apply(__method, args);
 234+ }, timeout);
 235+ },
 236+
 237+ wrap: function(wrapper) {
 238+ var __method = this;
 239+ return function() {
 240+ return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
 241+ }
 242+ },
 243+
 244+ methodize: function() {
 245+ if (this._methodized) return this._methodized;
 246+ var __method = this;
 247+ return this._methodized = function() {
 248+ return __method.apply(null, [this].concat($A(arguments)));
 249+ };
 250+ }
 251+});
 252+
 253+Function.prototype.defer = Function.prototype.delay.curry(0.01);
 254+
 255+Date.prototype.toJSON = function() {
 256+ return '"' + this.getUTCFullYear() + '-' +
 257+ (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
 258+ this.getUTCDate().toPaddedString(2) + 'T' +
 259+ this.getUTCHours().toPaddedString(2) + ':' +
 260+ this.getUTCMinutes().toPaddedString(2) + ':' +
 261+ this.getUTCSeconds().toPaddedString(2) + 'Z"';
 262+};
 263+
 264+var Try = {
 265+ these: function() {
 266+ var returnValue;
 267+
 268+ for (var i = 0, length = arguments.length; i < length; i++) {
 269+ var lambda = arguments[i];
 270+ try {
 271+ returnValue = lambda();
 272+ break;
 273+ } catch (e) { }
 274+ }
 275+
 276+ return returnValue;
 277+ }
 278+};
 279+
 280+RegExp.prototype.match = RegExp.prototype.test;
 281+
 282+RegExp.escape = function(str) {
 283+ return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
 284+};
 285+
 286+/*--------------------------------------------------------------------------*/
 287+
 288+var PeriodicalExecuter = Class.create({
 289+ initialize: function(callback, frequency) {
 290+ this.callback = callback;
 291+ this.frequency = frequency;
 292+ this.currentlyExecuting = false;
 293+
 294+ this.registerCallback();
 295+ },
 296+
 297+ registerCallback: function() {
 298+ this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
 299+ },
 300+
 301+ execute: function() {
 302+ this.callback(this);
 303+ },
 304+
 305+ stop: function() {
 306+ if (!this.timer) return;
 307+ clearInterval(this.timer);
 308+ this.timer = null;
 309+ },
 310+
 311+ onTimerEvent: function() {
 312+ if (!this.currentlyExecuting) {
 313+ try {
 314+ this.currentlyExecuting = true;
 315+ this.execute();
 316+ } finally {
 317+ this.currentlyExecuting = false;
 318+ }
 319+ }
 320+ }
 321+});
 322+Object.extend(String, {
 323+ interpret: function(value) {
 324+ return value == null ? '' : String(value);
 325+ },
 326+ specialChar: {
 327+ '\b': '\\b',
 328+ '\t': '\\t',
 329+ '\n': '\\n',
 330+ '\f': '\\f',
 331+ '\r': '\\r',
 332+ '\\': '\\\\'
 333+ }
 334+});
 335+
 336+Object.extend(String.prototype, {
 337+ gsub: function(pattern, replacement) {
 338+ var result = '', source = this, match;
 339+ replacement = arguments.callee.prepareReplacement(replacement);
 340+
 341+ while (source.length > 0) {
 342+ if (match = source.match(pattern)) {
 343+ result += source.slice(0, match.index);
 344+ result += String.interpret(replacement(match));
 345+ source = source.slice(match.index + match[0].length);
 346+ } else {
 347+ result += source, source = '';
 348+ }
 349+ }
 350+ return result;
 351+ },
 352+
 353+ sub: function(pattern, replacement, count) {
 354+ replacement = this.gsub.prepareReplacement(replacement);
 355+ count = count === undefined ? 1 : count;
 356+
 357+ return this.gsub(pattern, function(match) {
 358+ if (--count < 0) return match[0];
 359+ return replacement(match);
 360+ });
 361+ },
 362+
 363+ scan: function(pattern, iterator) {
 364+ this.gsub(pattern, iterator);
 365+ return String(this);
 366+ },
 367+
 368+ truncate: function(length, truncation) {
 369+ length = length || 30;
 370+ truncation = truncation === undefined ? '...' : truncation;
 371+ return this.length > length ?
 372+ this.slice(0, length - truncation.length) + truncation : String(this);
 373+ },
 374+
 375+ strip: function() {
 376+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
 377+ },
 378+
 379+ stripTags: function() {
 380+ return this.replace(/<\/?[^>]+>/gi, '');
 381+ },
 382+
 383+ stripScripts: function() {
 384+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
 385+ },
 386+
 387+ extractScripts: function() {
 388+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
 389+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
 390+ return (this.match(matchAll) || []).map(function(scriptTag) {
 391+ return (scriptTag.match(matchOne) || ['', ''])[1];
 392+ });
 393+ },
 394+
 395+ evalScripts: function() {
 396+ return this.extractScripts().map(function(script) { return eval(script) });
 397+ },
 398+
 399+ escapeHTML: function() {
 400+ var self = arguments.callee;
 401+ self.text.data = this;
 402+ return self.div.innerHTML;
 403+ },
 404+
 405+ unescapeHTML: function() {
 406+ var div = new Element('div');
 407+ div.innerHTML = this.stripTags();
 408+ return div.childNodes[0] ? (div.childNodes.length > 1 ?
 409+ $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
 410+ div.childNodes[0].nodeValue) : '';
 411+ },
 412+
 413+ toQueryParams: function(separator) {
 414+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
 415+ if (!match) return { };
 416+
 417+ return match[1].split(separator || '&').inject({ }, function(hash, pair) {
 418+ if ((pair = pair.split('='))[0]) {
 419+ var key = decodeURIComponent(pair.shift());
 420+ var value = pair.length > 1 ? pair.join('=') : pair[0];
 421+ if (value != undefined) value = decodeURIComponent(value);
 422+
 423+ if (key in hash) {
 424+ if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
 425+ hash[key].push(value);
 426+ }
 427+ else hash[key] = value;
 428+ }
 429+ return hash;
 430+ });
 431+ },
 432+
 433+ toArray: function() {
 434+ return this.split('');
 435+ },
 436+
 437+ succ: function() {
 438+ return this.slice(0, this.length - 1) +
 439+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
 440+ },
 441+
 442+ times: function(count) {
 443+ return count < 1 ? '' : new Array(count + 1).join(this);
 444+ },
 445+
 446+ camelize: function() {
 447+ var parts = this.split('-'), len = parts.length;
 448+ if (len == 1) return parts[0];
 449+
 450+ var camelized = this.charAt(0) == '-'
 451+ ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
 452+ : parts[0];
 453+
 454+ for (var i = 1; i < len; i++)
 455+ camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
 456+
 457+ return camelized;
 458+ },
 459+
 460+ capitalize: function() {
 461+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
 462+ },
 463+
 464+ underscore: function() {
 465+ return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
 466+ },
 467+
 468+ dasherize: function() {
 469+ return this.gsub(/_/,'-');
 470+ },
 471+
 472+ inspect: function(useDoubleQuotes) {
 473+ var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
 474+ var character = String.specialChar[match[0]];
 475+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
 476+ });
 477+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
 478+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
 479+ },
 480+
 481+ toJSON: function() {
 482+ return this.inspect(true);
 483+ },
 484+
 485+ unfilterJSON: function(filter) {
 486+ return this.sub(filter || Prototype.JSONFilter, '#{1}');
 487+ },
 488+
 489+ isJSON: function() {
 490+ var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
 491+ return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
 492+ },
 493+
 494+ evalJSON: function(sanitize) {
 495+ var json = this.unfilterJSON();
 496+ try {
 497+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
 498+ } catch (e) { }
 499+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
 500+ },
 501+
 502+ include: function(pattern) {
 503+ return this.indexOf(pattern) > -1;
 504+ },
 505+
 506+ startsWith: function(pattern) {
 507+ return this.indexOf(pattern) === 0;
 508+ },
 509+
 510+ endsWith: function(pattern) {
 511+ var d = this.length - pattern.length;
 512+ return d >= 0 && this.lastIndexOf(pattern) === d;
 513+ },
 514+
 515+ empty: function() {
 516+ return this == '';
 517+ },
 518+
 519+ blank: function() {
 520+ return /^\s*$/.test(this);
 521+ },
 522+
 523+ interpolate: function(object, pattern) {
 524+ return new Template(this, pattern).evaluate(object);
 525+ }
 526+});
 527+
 528+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
 529+ escapeHTML: function() {
 530+ return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
 531+ },
 532+ unescapeHTML: function() {
 533+ return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
 534+ }
 535+});
 536+
 537+String.prototype.gsub.prepareReplacement = function(replacement) {
 538+ if (Object.isFunction(replacement)) return replacement;
 539+ var template = new Template(replacement);
 540+ return function(match) { return template.evaluate(match) };
 541+};
 542+
 543+String.prototype.parseQuery = String.prototype.toQueryParams;
 544+
 545+Object.extend(String.prototype.escapeHTML, {
 546+ div: document.createElement('div'),
 547+ text: document.createTextNode('')
 548+});
 549+
 550+with (String.prototype.escapeHTML) div.appendChild(text);
 551+
 552+var Template = Class.create({
 553+ initialize: function(template, pattern) {
 554+ this.template = template.toString();
 555+ this.pattern = pattern || Template.Pattern;
 556+ },
 557+
 558+ evaluate: function(object) {
 559+ if (Object.isFunction(object.toTemplateReplacements))
 560+ object = object.toTemplateReplacements();
 561+
 562+ return this.template.gsub(this.pattern, function(match) {
 563+ if (object == null) return '';
 564+
 565+ var before = match[1] || '';
 566+ if (before == '\\') return match[2];
 567+
 568+ var ctx = object, expr = match[3];
 569+ var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/, match = pattern.exec(expr);
 570+ if (match == null) return before;
 571+
 572+ while (match != null) {
 573+ var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
 574+ ctx = ctx[comp];
 575+ if (null == ctx || '' == match[3]) break;
 576+ expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
 577+ match = pattern.exec(expr);
 578+ }
 579+
 580+ return before + String.interpret(ctx);
 581+ }.bind(this));
 582+ }
 583+});
 584+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
 585+
 586+var $break = { };
 587+
 588+var Enumerable = {
 589+ each: function(iterator, context) {
 590+ var index = 0;
 591+ iterator = iterator.bind(context);
 592+ try {
 593+ this._each(function(value) {
 594+ iterator(value, index++);
 595+ });
 596+ } catch (e) {
 597+ if (e != $break) throw e;
 598+ }
 599+ return this;
 600+ },
 601+
 602+ eachSlice: function(number, iterator, context) {
 603+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 604+ var index = -number, slices = [], array = this.toArray();
 605+ while ((index += number) < array.length)
 606+ slices.push(array.slice(index, index+number));
 607+ return slices.collect(iterator, context);
 608+ },
 609+
 610+ all: function(iterator, context) {
 611+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 612+ var result = true;
 613+ this.each(function(value, index) {
 614+ result = result && !!iterator(value, index);
 615+ if (!result) throw $break;
 616+ });
 617+ return result;
 618+ },
 619+
 620+ any: function(iterator, context) {
 621+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 622+ var result = false;
 623+ this.each(function(value, index) {
 624+ if (result = !!iterator(value, index))
 625+ throw $break;
 626+ });
 627+ return result;
 628+ },
 629+
 630+ collect: function(iterator, context) {
 631+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 632+ var results = [];
 633+ this.each(function(value, index) {
 634+ results.push(iterator(value, index));
 635+ });
 636+ return results;
 637+ },
 638+
 639+ detect: function(iterator, context) {
 640+ iterator = iterator.bind(context);
 641+ var result;
 642+ this.each(function(value, index) {
 643+ if (iterator(value, index)) {
 644+ result = value;
 645+ throw $break;
 646+ }
 647+ });
 648+ return result;
 649+ },
 650+
 651+ findAll: function(iterator, context) {
 652+ iterator = iterator.bind(context);
 653+ var results = [];
 654+ this.each(function(value, index) {
 655+ if (iterator(value, index))
 656+ results.push(value);
 657+ });
 658+ return results;
 659+ },
 660+
 661+ grep: function(filter, iterator, context) {
 662+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 663+ var results = [];
 664+
 665+ if (Object.isString(filter))
 666+ filter = new RegExp(filter);
 667+
 668+ this.each(function(value, index) {
 669+ if (filter.match(value))
 670+ results.push(iterator(value, index));
 671+ });
 672+ return results;
 673+ },
 674+
 675+ include: function(object) {
 676+ if (Object.isFunction(this.indexOf))
 677+ if (this.indexOf(object) != -1) return true;
 678+
 679+ var found = false;
 680+ this.each(function(value) {
 681+ if (value == object) {
 682+ found = true;
 683+ throw $break;
 684+ }
 685+ });
 686+ return found;
 687+ },
 688+
 689+ inGroupsOf: function(number, fillWith) {
 690+ fillWith = fillWith === undefined ? null : fillWith;
 691+ return this.eachSlice(number, function(slice) {
 692+ while(slice.length < number) slice.push(fillWith);
 693+ return slice;
 694+ });
 695+ },
 696+
 697+ inject: function(memo, iterator, context) {
 698+ iterator = iterator.bind(context);
 699+ this.each(function(value, index) {
 700+ memo = iterator(memo, value, index);
 701+ });
 702+ return memo;
 703+ },
 704+
 705+ invoke: function(method) {
 706+ var args = $A(arguments).slice(1);
 707+ return this.map(function(value) {
 708+ return value[method].apply(value, args);
 709+ });
 710+ },
 711+
 712+ max: function(iterator, context) {
 713+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 714+ var result;
 715+ this.each(function(value, index) {
 716+ value = iterator(value, index);
 717+ if (result == undefined || value >= result)
 718+ result = value;
 719+ });
 720+ return result;
 721+ },
 722+
 723+ min: function(iterator, context) {
 724+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 725+ var result;
 726+ this.each(function(value, index) {
 727+ value = iterator(value, index);
 728+ if (result == undefined || value < result)
 729+ result = value;
 730+ });
 731+ return result;
 732+ },
 733+
 734+ partition: function(iterator, context) {
 735+ iterator = iterator ? iterator.bind(context) : Prototype.K;
 736+ var trues = [], falses = [];
 737+ this.each(function(value, index) {
 738+ (iterator(value, index) ?
 739+ trues : falses).push(value);
 740+ });
 741+ return [trues, falses];
 742+ },
 743+
 744+ pluck: function(property) {
 745+ var results = [];
 746+ this.each(function(value) {
 747+ results.push(value[property]);
 748+ });
 749+ return results;
 750+ },
 751+
 752+ reject: function(iterator, context) {
 753+ iterator = iterator.bind(context);
 754+ var results = [];
 755+ this.each(function(value, index) {
 756+ if (!iterator(value, index))
 757+ results.push(value);
 758+ });
 759+ return results;
 760+ },
 761+
 762+ sortBy: function(iterator, context) {
 763+ iterator = iterator.bind(context);
 764+ return this.map(function(value, index) {
 765+ return {value: value, criteria: iterator(value, index)};
 766+ }).sort(function(left, right) {
 767+ var a = left.criteria, b = right.criteria;
 768+ return a < b ? -1 : a > b ? 1 : 0;
 769+ }).pluck('value');
 770+ },
 771+
 772+ toArray: function() {
 773+ return this.map();
 774+ },
 775+
 776+ zip: function() {
 777+ var iterator = Prototype.K, args = $A(arguments);
 778+ if (Object.isFunction(args.last()))
 779+ iterator = args.pop();
 780+
 781+ var collections = [this].concat(args).map($A);
 782+ return this.map(function(value, index) {
 783+ return iterator(collections.pluck(index));
 784+ });
 785+ },
 786+
 787+ size: function() {
 788+ return this.toArray().length;
 789+ },
 790+
 791+ inspect: function() {
 792+ return '#<Enumerable:' + this.toArray().inspect() + '>';
 793+ }
 794+};
 795+
 796+Object.extend(Enumerable, {
 797+ map: Enumerable.collect,
 798+ find: Enumerable.detect,
 799+ select: Enumerable.findAll,
 800+ filter: Enumerable.findAll,
 801+ member: Enumerable.include,
 802+ entries: Enumerable.toArray,
 803+ every: Enumerable.all,
 804+ some: Enumerable.any
 805+});
 806+function $A(iterable) {
 807+ if (!iterable) return [];
 808+ if (iterable.toArray) return iterable.toArray();
 809+ var length = iterable.length, results = new Array(length);
 810+ while (length--) results[length] = iterable[length];
 811+ return results;
 812+}
 813+
 814+if (Prototype.Browser.WebKit) {
 815+ function $A(iterable) {
 816+ if (!iterable) return [];
 817+ if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
 818+ iterable.toArray) return iterable.toArray();
 819+ var length = iterable.length, results = new Array(length);
 820+ while (length--) results[length] = iterable[length];
 821+ return results;
 822+ }
 823+}
 824+
 825+Array.from = $A;
 826+
 827+Object.extend(Array.prototype, Enumerable);
 828+
 829+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
 830+
 831+Object.extend(Array.prototype, {
 832+ _each: function(iterator) {
 833+ for (var i = 0, length = this.length; i < length; i++)
 834+ iterator(this[i]);
 835+ },
 836+
 837+ clear: function() {
 838+ this.length = 0;
 839+ return this;
 840+ },
 841+
 842+ first: function() {
 843+ return this[0];
 844+ },
 845+
 846+ last: function() {
 847+ return this[this.length - 1];
 848+ },
 849+
 850+ compact: function() {
 851+ return this.select(function(value) {
 852+ return value != null;
 853+ });
 854+ },
 855+
 856+ flatten: function() {
 857+ return this.inject([], function(array, value) {
 858+ return array.concat(Object.isArray(value) ?
 859+ value.flatten() : [value]);
 860+ });
 861+ },
 862+
 863+ without: function() {
 864+ var values = $A(arguments);
 865+ return this.select(function(value) {
 866+ return !values.include(value);
 867+ });
 868+ },
 869+
 870+ reverse: function(inline) {
 871+ return (inline !== false ? this : this.toArray())._reverse();
 872+ },
 873+
 874+ reduce: function() {
 875+ return this.length > 1 ? this : this[0];
 876+ },
 877+
 878+ uniq: function(sorted) {
 879+ return this.inject([], function(array, value, index) {
 880+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
 881+ array.push(value);
 882+ return array;
 883+ });
 884+ },
 885+
 886+ intersect: function(array) {
 887+ return this.uniq().findAll(function(item) {
 888+ return array.detect(function(value) { return item === value });
 889+ });
 890+ },
 891+
 892+ clone: function() {
 893+ return [].concat(this);
 894+ },
 895+
 896+ size: function() {
 897+ return this.length;
 898+ },
 899+
 900+ inspect: function() {
 901+ return '[' + this.map(Object.inspect).join(', ') + ']';
 902+ },
 903+
 904+ toJSON: function() {
 905+ var results = [];
 906+ this.each(function(object) {
 907+ var value = Object.toJSON(object);
 908+ if (value !== undefined) results.push(value);
 909+ });
 910+ return '[' + results.join(', ') + ']';
 911+ }
 912+});
 913+
 914+// use native browser JS 1.6 implementation if available
 915+if (Object.isFunction(Array.prototype.forEach))
 916+ Array.prototype._each = Array.prototype.forEach;
 917+
 918+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
 919+ i || (i = 0);
 920+ var length = this.length;
 921+ if (i < 0) i = length + i;
 922+ for (; i < length; i++)
 923+ if (this[i] === item) return i;
 924+ return -1;
 925+};
 926+
 927+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
 928+ i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
 929+ var n = this.slice(0, i).reverse().indexOf(item);
 930+ return (n < 0) ? n : i - n - 1;
 931+};
 932+
 933+Array.prototype.toArray = Array.prototype.clone;
 934+
 935+function $w(string) {
 936+ if (!Object.isString(string)) return [];
 937+ string = string.strip();
 938+ return string ? string.split(/\s+/) : [];
 939+}
 940+
 941+if (Prototype.Browser.Opera){
 942+ Array.prototype.concat = function() {
 943+ var array = [];
 944+ for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
 945+ for (var i = 0, length = arguments.length; i < length; i++) {
 946+ if (Object.isArray(arguments[i])) {
 947+ for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
 948+ array.push(arguments[i][j]);
 949+ } else {
 950+ array.push(arguments[i]);
 951+ }
 952+ }
 953+ return array;
 954+ };
 955+}
 956+Object.extend(Number.prototype, {
 957+ toColorPart: function() {
 958+ return this.toPaddedString(2, 16);
 959+ },
 960+
 961+ succ: function() {
 962+ return this + 1;
 963+ },
 964+
 965+ times: function(iterator) {
 966+ $R(0, this, true).each(iterator);
 967+ return this;
 968+ },
 969+
 970+ toPaddedString: function(length, radix) {
 971+ var string = this.toString(radix || 10);
 972+ return '0'.times(length - string.length) + string;
 973+ },
 974+
 975+ toJSON: function() {
 976+ return isFinite(this) ? this.toString() : 'null';
 977+ }
 978+});
 979+
 980+$w('abs round ceil floor').each(function(method){
 981+ Number.prototype[method] = Math[method].methodize();
 982+});
 983+function $H(object) {
 984+ return new Hash(object);
 985+};
 986+
 987+var Hash = Class.create(Enumerable, (function() {
 988+ if (function() {
 989+ var i = 0, Test = function(value) { this.key = value };
 990+ Test.prototype.key = 'foo';
 991+ for (var property in new Test('bar')) i++;
 992+ return i > 1;
 993+ }()) {
 994+ function each(iterator) {
 995+ var cache = [];
 996+ for (var key in this._object) {
 997+ var value = this._object[key];
 998+ if (cache.include(key)) continue;
 999+ cache.push(key);
 1000+ var pair = [key, value];
 1001+ pair.key = key;
 1002+ pair.value = value;
 1003+ iterator(pair);
 1004+ }
 1005+ }
 1006+ } else {
 1007+ function each(iterator) {
 1008+ for (var key in this._object) {
 1009+ var value = this._object[key], pair = [key, value];
 1010+ pair.key = key;
 1011+ pair.value = value;
 1012+ iterator(pair);
 1013+ }
 1014+ }
 1015+ }
 1016+
 1017+ function toQueryPair(key, value) {
 1018+ if (Object.isUndefined(value)) return key;
 1019+ return key + '=' + encodeURIComponent(String.interpret(value));
 1020+ }
 1021+
 1022+ return {
 1023+ initialize: function(object) {
 1024+ this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
 1025+ },
 1026+
 1027+ _each: each,
 1028+
 1029+ set: function(key, value) {
 1030+ return this._object[key] = value;
 1031+ },
 1032+
 1033+ get: function(key) {
 1034+ return this._object[key];
 1035+ },
 1036+
 1037+ unset: function(key) {
 1038+ var value = this._object[key];
 1039+ delete this._object[key];
 1040+ return value;
 1041+ },
 1042+
 1043+ toObject: function() {
 1044+ return Object.clone(this._object);
 1045+ },
 1046+
 1047+ keys: function() {
 1048+ return this.pluck('key');
 1049+ },
 1050+
 1051+ values: function() {
 1052+ return this.pluck('value');
 1053+ },
 1054+
 1055+ index: function(value) {
 1056+ var match = this.detect(function(pair) {
 1057+ return pair.value === value;
 1058+ });
 1059+ return match && match.key;
 1060+ },
 1061+
 1062+ merge: function(object) {
 1063+ return this.clone().update(object);
 1064+ },
 1065+
 1066+ update: function(object) {
 1067+ return new Hash(object).inject(this, function(result, pair) {
 1068+ result.set(pair.key, pair.value);
 1069+ return result;
 1070+ });
 1071+ },
 1072+
 1073+ toQueryString: function() {
 1074+ return this.map(function(pair) {
 1075+ var key = encodeURIComponent(pair.key), values = pair.value;
 1076+
 1077+ if (values && typeof values == 'object') {
 1078+ if (Object.isArray(values))
 1079+ return values.map(toQueryPair.curry(key)).join('&');
 1080+ }
 1081+ return toQueryPair(key, values);
 1082+ }).join('&');
 1083+ },
 1084+
 1085+ inspect: function() {
 1086+ return '#<Hash:{' + this.map(function(pair) {
 1087+ return pair.map(Object.inspect).join(': ');
 1088+ }).join(', ') + '}>';
 1089+ },
 1090+
 1091+ toJSON: function() {
 1092+ return Object.toJSON(this.toObject());
 1093+ },
 1094+
 1095+ clone: function() {
 1096+ return new Hash(this);
 1097+ }
 1098+ }
 1099+})());
 1100+
 1101+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
 1102+Hash.from = $H;
 1103+var ObjectRange = Class.create(Enumerable, {
 1104+ initialize: function(start, end, exclusive) {
 1105+ this.start = start;
 1106+ this.end = end;
 1107+ this.exclusive = exclusive;
 1108+ },
 1109+
 1110+ _each: function(iterator) {
 1111+ var value = this.start;
 1112+ while (this.include(value)) {
 1113+ iterator(value);
 1114+ value = value.succ();
 1115+ }
 1116+ },
 1117+
 1118+ include: function(value) {
 1119+ if (value < this.start)
 1120+ return false;
 1121+ if (this.exclusive)
 1122+ return value < this.end;
 1123+ return value <= this.end;
 1124+ }
 1125+});
 1126+
 1127+var $R = function(start, end, exclusive) {
 1128+ return new ObjectRange(start, end, exclusive);
 1129+};
 1130+
 1131+var Ajax = {
 1132+ getTransport: function() {
 1133+ return Try.these(
 1134+ function() {return new XMLHttpRequest()},
 1135+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
 1136+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
 1137+ ) || false;
 1138+ },
 1139+
 1140+ activeRequestCount: 0
 1141+};
 1142+
 1143+Ajax.Responders = {
 1144+ responders: [],
 1145+
 1146+ _each: function(iterator) {
 1147+ this.responders._each(iterator);
 1148+ },
 1149+
 1150+ register: function(responder) {
 1151+ if (!this.include(responder))
 1152+ this.responders.push(responder);
 1153+ },
 1154+
 1155+ unregister: function(responder) {
 1156+ this.responders = this.responders.without(responder);
 1157+ },
 1158+
 1159+ dispatch: function(callback, request, transport, json) {
 1160+ this.each(function(responder) {
 1161+ if (Object.isFunction(responder[callback])) {
 1162+ try {
 1163+ responder[callback].apply(responder, [request, transport, json]);
 1164+ } catch (e) { }
 1165+ }
 1166+ });
 1167+ }
 1168+};
 1169+
 1170+Object.extend(Ajax.Responders, Enumerable);
 1171+
 1172+Ajax.Responders.register({
 1173+ onCreate: function() { Ajax.activeRequestCount++ },
 1174+ onComplete: function() { Ajax.activeRequestCount-- }
 1175+});
 1176+
 1177+Ajax.Base = Class.create({
 1178+ initialize: function(options) {
 1179+ this.options = {
 1180+ method: 'post',
 1181+ asynchronous: true,
 1182+ contentType: 'application/x-www-form-urlencoded',
 1183+ encoding: 'UTF-8',
 1184+ parameters: '',
 1185+ evalJSON: true,
 1186+ evalJS: true
 1187+ };
 1188+ Object.extend(this.options, options || { });
 1189+
 1190+ this.options.method = this.options.method.toLowerCase();
 1191+ if (Object.isString(this.options.parameters))
 1192+ this.options.parameters = this.options.parameters.toQueryParams();
 1193+ }
 1194+});
 1195+
 1196+Ajax.Request = Class.create(Ajax.Base, {
 1197+ _complete: false,
 1198+
 1199+ initialize: function($super, url, options) {
 1200+ $super(options);
 1201+ this.transport = Ajax.getTransport();
 1202+ this.request(url);
 1203+ },
 1204+
 1205+ request: function(url) {
 1206+ this.url = url;
 1207+ this.method = this.options.method;
 1208+ var params = Object.clone(this.options.parameters);
 1209+
 1210+ if (!['get', 'post'].include(this.method)) {
 1211+ // simulate other verbs over post
 1212+ params['_method'] = this.method;
 1213+ this.method = 'post';
 1214+ }
 1215+
 1216+ this.parameters = params;
 1217+
 1218+ if (params = Object.toQueryString(params)) {
 1219+ // when GET, append parameters to URL
 1220+ if (this.method == 'get')
 1221+ this.url += (this.url.include('?') ? '&' : '?') + params;
 1222+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
 1223+ params += '&_=';
 1224+ }
 1225+
 1226+ try {
 1227+ var response = new Ajax.Response(this);
 1228+ if (this.options.onCreate) this.options.onCreate(response);
 1229+ Ajax.Responders.dispatch('onCreate', this, response);
 1230+
 1231+ this.transport.open(this.method.toUpperCase(), this.url,
 1232+ this.options.asynchronous);
 1233+
 1234+ if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
 1235+
 1236+ this.transport.onreadystatechange = this.onStateChange.bind(this);
 1237+ this.setRequestHeaders();
 1238+
 1239+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
 1240+ this.transport.send(this.body);
 1241+
 1242+ /* Force Firefox to handle ready state 4 for synchronous requests */
 1243+ if (!this.options.asynchronous && this.transport.overrideMimeType)
 1244+ this.onStateChange();
 1245+
 1246+ }
 1247+ catch (e) {
 1248+ this.dispatchException(e);
 1249+ }
 1250+ },
 1251+
 1252+ onStateChange: function() {
 1253+ var readyState = this.transport.readyState;
 1254+ if (readyState > 1 && !((readyState == 4) && this._complete))
 1255+ this.respondToReadyState(this.transport.readyState);
 1256+ },
 1257+
 1258+ setRequestHeaders: function() {
 1259+ var headers = {
 1260+ 'X-Requested-With': 'XMLHttpRequest',
 1261+ 'X-Prototype-Version': Prototype.Version,
 1262+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
 1263+ };
 1264+
 1265+ if (this.method == 'post') {
 1266+ headers['Content-type'] = this.options.contentType +
 1267+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
 1268+
 1269+ /* Force "Connection: close" for older Mozilla browsers to work
 1270+ * around a bug where XMLHttpRequest sends an incorrect
 1271+ * Content-length header. See Mozilla Bugzilla #246651.
 1272+ */
 1273+ if (this.transport.overrideMimeType &&
 1274+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
 1275+ headers['Connection'] = 'close';
 1276+ }
 1277+
 1278+ // user-defined headers
 1279+ if (typeof this.options.requestHeaders == 'object') {
 1280+ var extras = this.options.requestHeaders;
 1281+
 1282+ if (Object.isFunction(extras.push))
 1283+ for (var i = 0, length = extras.length; i < length; i += 2)
 1284+ headers[extras[i]] = extras[i+1];
 1285+ else
 1286+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
 1287+ }
 1288+
 1289+ for (var name in headers)
 1290+ this.transport.setRequestHeader(name, headers[name]);
 1291+ },
 1292+
 1293+ success: function() {
 1294+ var status = this.getStatus();
 1295+ return !status || (status >= 200 && status < 300);
 1296+ },
 1297+
 1298+ getStatus: function() {
 1299+ try {
 1300+ return this.transport.status || 0;
 1301+ } catch (e) { return 0 }
 1302+ },
 1303+
 1304+ respondToReadyState: function(readyState) {
 1305+ var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
 1306+
 1307+ if (state == 'Complete') {
 1308+ try {
 1309+ this._complete = true;
 1310+ (this.options['on' + response.status]
 1311+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
 1312+ || Prototype.emptyFunction)(response, response.headerJSON);
 1313+ } catch (e) {
 1314+ this.dispatchException(e);
 1315+ }
 1316+
 1317+ var contentType = response.getHeader('Content-type');
 1318+ if (this.options.evalJS == 'force'
 1319+ || (this.options.evalJS && contentType
 1320+ && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
 1321+ this.evalResponse();
 1322+ }
 1323+
 1324+ try {
 1325+ (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
 1326+ Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
 1327+ } catch (e) {
 1328+ this.dispatchException(e);
 1329+ }
 1330+
 1331+ if (state == 'Complete') {
 1332+ // avoid memory leak in MSIE: clean up
 1333+ this.transport.onreadystatechange = Prototype.emptyFunction;
 1334+ }
 1335+ },
 1336+
 1337+ getHeader: function(name) {
 1338+ try {
 1339+ return this.transport.getResponseHeader(name);
 1340+ } catch (e) { return null }
 1341+ },
 1342+
 1343+ evalResponse: function() {
 1344+ try {
 1345+ return eval((this.transport.responseText || '').unfilterJSON());
 1346+ } catch (e) {
 1347+ this.dispatchException(e);
 1348+ }
 1349+ },
 1350+
 1351+ dispatchException: function(exception) {
 1352+ (this.options.onException || Prototype.emptyFunction)(this, exception);
 1353+ Ajax.Responders.dispatch('onException', this, exception);
 1354+ }
 1355+});
 1356+
 1357+Ajax.Request.Events =
 1358+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
 1359+
 1360+Ajax.Response = Class.create({
 1361+ initialize: function(request){
 1362+ this.request = request;
 1363+ var transport = this.transport = request.transport,
 1364+ readyState = this.readyState = transport.readyState;
 1365+
 1366+ if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
 1367+ this.status = this.getStatus();
 1368+ this.statusText = this.getStatusText();
 1369+ this.responseText = String.interpret(transport.responseText);
 1370+ this.headerJSON = this._getHeaderJSON();
 1371+ }
 1372+
 1373+ if(readyState == 4) {
 1374+ var xml = transport.responseXML;
 1375+ this.responseXML = xml === undefined ? null : xml;
 1376+ this.responseJSON = this._getResponseJSON();
 1377+ }
 1378+ },
 1379+
 1380+ status: 0,
 1381+ statusText: '',
 1382+
 1383+ getStatus: Ajax.Request.prototype.getStatus,
 1384+
 1385+ getStatusText: function() {
 1386+ try {
 1387+ return this.transport.statusText || '';
 1388+ } catch (e) { return '' }
 1389+ },
 1390+
 1391+ getHeader: Ajax.Request.prototype.getHeader,
 1392+
 1393+ getAllHeaders: function() {
 1394+ try {
 1395+ return this.getAllResponseHeaders();
 1396+ } catch (e) { return null }
 1397+ },
 1398+
 1399+ getResponseHeader: function(name) {
 1400+ return this.transport.getResponseHeader(name);
 1401+ },
 1402+
 1403+ getAllResponseHeaders: function() {
 1404+ return this.transport.getAllResponseHeaders();
 1405+ },
 1406+
 1407+ _getHeaderJSON: function() {
 1408+ var json = this.getHeader('X-JSON');
 1409+ if (!json) return null;
 1410+ json = decodeURIComponent(escape(json));
 1411+ try {
 1412+ return json.evalJSON(this.request.options.sanitizeJSON);
 1413+ } catch (e) {
 1414+ this.request.dispatchException(e);
 1415+ }
 1416+ },
 1417+
 1418+ _getResponseJSON: function() {
 1419+ var options = this.request.options;
 1420+ if (!options.evalJSON || (options.evalJSON != 'force' &&
 1421+ !(this.getHeader('Content-type') || '').include('application/json')))
 1422+ return null;
 1423+ try {
 1424+ return this.transport.responseText.evalJSON(options.sanitizeJSON);
 1425+ } catch (e) {
 1426+ this.request.dispatchException(e);
 1427+ }
 1428+ }
 1429+});
 1430+
 1431+Ajax.Updater = Class.create(Ajax.Request, {
 1432+ initialize: function($super, container, url, options) {
 1433+ this.container = {
 1434+ success: (container.success || container),
 1435+ failure: (container.failure || (container.success ? null : container))
 1436+ };
 1437+
 1438+ options = options || { };
 1439+ var onComplete = options.onComplete;
 1440+ options.onComplete = (function(response, param) {
 1441+ this.updateContent(response.responseText);
 1442+ if (Object.isFunction(onComplete)) onComplete(response, param);
 1443+ }).bind(this);
 1444+
 1445+ $super(url, options);
 1446+ },
 1447+
 1448+ updateContent: function(responseText) {
 1449+ var receiver = this.container[this.success() ? 'success' : 'failure'],
 1450+ options = this.options;
 1451+
 1452+ if (!options.evalScripts) responseText = responseText.stripScripts();
 1453+
 1454+ if (receiver = $(receiver)) {
 1455+ if (options.insertion) {
 1456+ if (Object.isString(options.insertion)) {
 1457+ var insertion = { }; insertion[options.insertion] = responseText;
 1458+ receiver.insert(insertion);
 1459+ }
 1460+ else options.insertion(receiver, responseText);
 1461+ }
 1462+ else receiver.update(responseText);
 1463+ }
 1464+
 1465+ if (this.success()) {
 1466+ if (this.onComplete) this.onComplete.bind(this).defer();
 1467+ }
 1468+ }
 1469+});
 1470+
 1471+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
 1472+ initialize: function($super, container, url, options) {
 1473+ $super(options);
 1474+ this.onComplete = this.options.onComplete;
 1475+
 1476+ this.frequency = (this.options.frequency || 2);
 1477+ this.decay = (this.options.decay || 1);
 1478+
 1479+ this.updater = { };
 1480+ this.container = container;
 1481+ this.url = url;
 1482+
 1483+ this.start();
 1484+ },
 1485+
 1486+ start: function() {
 1487+ this.options.onComplete = this.updateComplete.bind(this);
 1488+ this.onTimerEvent();
 1489+ },
 1490+
 1491+ stop: function() {
 1492+ this.updater.options.onComplete = undefined;
 1493+ clearTimeout(this.timer);
 1494+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
 1495+ },
 1496+
 1497+ updateComplete: function(response) {
 1498+ if (this.options.decay) {
 1499+ this.decay = (response.responseText == this.lastText ?
 1500+ this.decay * this.options.decay : 1);
 1501+
 1502+ this.lastText = response.responseText;
 1503+ }
 1504+ this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
 1505+ },
 1506+
 1507+ onTimerEvent: function() {
 1508+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
 1509+ }
 1510+});
 1511+function $(element) {
 1512+ if (arguments.length > 1) {
 1513+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
 1514+ elements.push($(arguments[i]));
 1515+ return elements;
 1516+ }
 1517+ if (Object.isString(element))
 1518+ element = document.getElementById(element);
 1519+ return Element.extend(element);
 1520+}
 1521+
 1522+if (Prototype.BrowserFeatures.XPath) {
 1523+ document._getElementsByXPath = function(expression, parentElement) {
 1524+ var results = [];
 1525+ var query = document.evaluate(expression, $(parentElement) || document,
 1526+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
 1527+ for (var i = 0, length = query.snapshotLength; i < length; i++)
 1528+ results.push(Element.extend(query.snapshotItem(i)));
 1529+ return results;
 1530+ };
 1531+}
 1532+
 1533+/*--------------------------------------------------------------------------*/
 1534+
 1535+if (!window.Node) var Node = { };
 1536+
 1537+if (!Node.ELEMENT_NODE) {
 1538+ // DOM level 2 ECMAScript Language Binding
 1539+ Object.extend(Node, {
 1540+ ELEMENT_NODE: 1,
 1541+ ATTRIBUTE_NODE: 2,
 1542+ TEXT_NODE: 3,
 1543+ CDATA_SECTION_NODE: 4,
 1544+ ENTITY_REFERENCE_NODE: 5,
 1545+ ENTITY_NODE: 6,
 1546+ PROCESSING_INSTRUCTION_NODE: 7,
 1547+ COMMENT_NODE: 8,
 1548+ DOCUMENT_NODE: 9,
 1549+ DOCUMENT_TYPE_NODE: 10,
 1550+ DOCUMENT_FRAGMENT_NODE: 11,
 1551+ NOTATION_NODE: 12
 1552+ });
 1553+}
 1554+
 1555+(function() {
 1556+ var element = this.Element;
 1557+ this.Element = function(tagName, attributes) {
 1558+ attributes = attributes || { };
 1559+ tagName = tagName.toLowerCase();
 1560+ var cache = Element.cache;
 1561+ if (Prototype.Browser.IE && attributes.name) {
 1562+ tagName = '<' + tagName + ' name="' + attributes.name + '">';
 1563+ delete attributes.name;
 1564+ return Element.writeAttribute(document.createElement(tagName), attributes);
 1565+ }
 1566+ if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
 1567+ return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
 1568+ };
 1569+ Object.extend(this.Element, element || { });
 1570+}).call(window);
 1571+
 1572+Element.cache = { };
 1573+
 1574+Element.Methods = {
 1575+ visible: function(element) {
 1576+ return $(element).style.display != 'none';
 1577+ },
 1578+
 1579+ toggle: function(element) {
 1580+ element = $(element);
 1581+ Element[Element.visible(element) ? 'hide' : 'show'](element);
 1582+ return element;
 1583+ },
 1584+
 1585+ hide: function(element) {
 1586+ $(element).style.display = 'none';
 1587+ return element;
 1588+ },
 1589+
 1590+ show: function(element) {
 1591+ $(element).style.display = '';
 1592+ return element;
 1593+ },
 1594+
 1595+ remove: function(element) {
 1596+ element = $(element);
 1597+ element.parentNode.removeChild(element);
 1598+ return element;
 1599+ },
 1600+
 1601+ update: function(element, content) {
 1602+ element = $(element);
 1603+ if (content && content.toElement) content = content.toElement();
 1604+ if (Object.isElement(content)) return element.update().insert(content);
 1605+ content = Object.toHTML(content);
 1606+ element.innerHTML = content.stripScripts();
 1607+ content.evalScripts.bind(content).defer();
 1608+ return element;
 1609+ },
 1610+
 1611+ replace: function(element, content) {
 1612+ element = $(element);
 1613+ if (content && content.toElement) content = content.toElement();
 1614+ else if (!Object.isElement(content)) {
 1615+ content = Object.toHTML(content);
 1616+ var range = element.ownerDocument.createRange();
 1617+ range.selectNode(element);
 1618+ content.evalScripts.bind(content).defer();
 1619+ content = range.createContextualFragment(content.stripScripts());
 1620+ }
 1621+ element.parentNode.replaceChild(content, element);
 1622+ return element;
 1623+ },
 1624+
 1625+ insert: function(element, insertions) {
 1626+ element = $(element);
 1627+
 1628+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
 1629+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
 1630+ insertions = {bottom:insertions};
 1631+
 1632+ var content, t, range;
 1633+
 1634+ for (position in insertions) {
 1635+ content = insertions[position];
 1636+ position = position.toLowerCase();
 1637+ t = Element._insertionTranslations[position];
 1638+
 1639+ if (content && content.toElement) content = content.toElement();
 1640+ if (Object.isElement(content)) {
 1641+ t.insert(element, content);
 1642+ continue;
 1643+ }
 1644+
 1645+ content = Object.toHTML(content);
 1646+
 1647+ range = element.ownerDocument.createRange();
 1648+ t.initializeRange(element, range);
 1649+ t.insert(element, range.createContextualFragment(content.stripScripts()));
 1650+
 1651+ content.evalScripts.bind(content).defer();
 1652+ }
 1653+
 1654+ return element;
 1655+ },
 1656+
 1657+ wrap: function(element, wrapper, attributes) {
 1658+ element = $(element);
 1659+ if (Object.isElement(wrapper))
 1660+ $(wrapper).writeAttribute(attributes || { });
 1661+ else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
 1662+ else wrapper = new Element('div', wrapper);
 1663+ if (element.parentNode)
 1664+ element.parentNode.replaceChild(wrapper, element);
 1665+ wrapper.appendChild(element);
 1666+ return wrapper;
 1667+ },
 1668+
 1669+ inspect: function(element) {
 1670+ element = $(element);
 1671+ var result = '<' + element.tagName.toLowerCase();
 1672+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
 1673+ var property = pair.first(), attribute = pair.last();
 1674+ var value = (element[property] || '').toString();
 1675+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
 1676+ });
 1677+ return result + '>';
 1678+ },
 1679+
 1680+ recursivelyCollect: function(element, property) {
 1681+ element = $(element);
 1682+ var elements = [];
 1683+ while (element = element[property])
 1684+ if (element.nodeType == 1)
 1685+ elements.push(Element.extend(element));
 1686+ return elements;
 1687+ },
 1688+
 1689+ ancestors: function(element) {
 1690+ return $(element).recursivelyCollect('parentNode');
 1691+ },
 1692+
 1693+ descendants: function(element) {
 1694+ return $A($(element).getElementsByTagName('*')).each(Element.extend);
 1695+ },
 1696+
 1697+ firstDescendant: function(element) {
 1698+ element = $(element).firstChild;
 1699+ while (element && element.nodeType != 1) element = element.nextSibling;
 1700+ return $(element);
 1701+ },
 1702+
 1703+ immediateDescendants: function(element) {
 1704+ if (!(element = $(element).firstChild)) return [];
 1705+ while (element && element.nodeType != 1) element = element.nextSibling;
 1706+ if (element) return [element].concat($(element).nextSiblings());
 1707+ return [];
 1708+ },
 1709+
 1710+ previousSiblings: function(element) {
 1711+ return $(element).recursivelyCollect('previousSibling');
 1712+ },
 1713+
 1714+ nextSiblings: function(element) {
 1715+ return $(element).recursivelyCollect('nextSibling');
 1716+ },
 1717+
 1718+ siblings: function(element) {
 1719+ element = $(element);
 1720+ return element.previousSiblings().reverse().concat(element.nextSiblings());
 1721+ },
 1722+
 1723+ match: function(element, selector) {
 1724+ if (Object.isString(selector))
 1725+ selector = new Selector(selector);
 1726+ return selector.match($(element));
 1727+ },
 1728+
 1729+ up: function(element, expression, index) {
 1730+ element = $(element);
 1731+ if (arguments.length == 1) return $(element.parentNode);
 1732+ var ancestors = element.ancestors();
 1733+ return expression ? Selector.findElement(ancestors, expression, index) :
 1734+ ancestors[index || 0];
 1735+ },
 1736+
 1737+ down: function(element, expression, index) {
 1738+ element = $(element);
 1739+ if (arguments.length == 1) return element.firstDescendant();
 1740+ var descendants = element.descendants();
 1741+ return expression ? Selector.findElement(descendants, expression, index) :
 1742+ descendants[index || 0];
 1743+ },
 1744+
 1745+ previous: function(element, expression, index) {
 1746+ element = $(element);
 1747+ if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
 1748+ var previousSiblings = element.previousSiblings();
 1749+ return expression ? Selector.findElement(previousSiblings, expression, index) :
 1750+ previousSiblings[index || 0];
 1751+ },
 1752+
 1753+ next: function(element, expression, index) {
 1754+ element = $(element);
 1755+ if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
 1756+ var nextSiblings = element.nextSiblings();
 1757+ return expression ? Selector.findElement(nextSiblings, expression, index) :
 1758+ nextSiblings[index || 0];
 1759+ },
 1760+
 1761+ select: function() {
 1762+ var args = $A(arguments), element = $(args.shift());
 1763+ return Selector.findChildElements(element, args);
 1764+ },
 1765+
 1766+ adjacent: function() {
 1767+ var args = $A(arguments), element = $(args.shift());
 1768+ return Selector.findChildElements(element.parentNode, args).without(element);
 1769+ },
 1770+
 1771+ identify: function(element) {
 1772+ element = $(element);
 1773+ var id = element.readAttribute('id'), self = arguments.callee;
 1774+ if (id) return id;
 1775+ do { id = 'anonymous_element_' + self.counter++ } while ($(id));
 1776+ element.writeAttribute('id', id);
 1777+ return id;
 1778+ },
 1779+
 1780+ readAttribute: function(element, name) {
 1781+ element = $(element);
 1782+ if (Prototype.Browser.IE) {
 1783+ var t = Element._attributeTranslations.read;
 1784+ if (t.values[name]) return t.values[name](element, name);
 1785+ if (t.names[name]) name = t.names[name];
 1786+ if (name.include(':')) {
 1787+ return (!element.attributes || !element.attributes[name]) ? null :
 1788+ element.attributes[name].value;
 1789+ }
 1790+ }
 1791+ return element.getAttribute(name);
 1792+ },
 1793+
 1794+ writeAttribute: function(element, name, value) {
 1795+ element = $(element);
 1796+ var attributes = { }, t = Element._attributeTranslations.write;
 1797+
 1798+ if (typeof name == 'object') attributes = name;
 1799+ else attributes[name] = value === undefined ? true : value;
 1800+
 1801+ for (var attr in attributes) {
 1802+ var name = t.names[attr] || attr, value = attributes[attr];
 1803+ if (t.values[attr]) name = t.values[attr](element, value);
 1804+ if (value === false || value === null)
 1805+ element.removeAttribute(name);
 1806+ else if (value === true)
 1807+ element.setAttribute(name, name);
 1808+ else element.setAttribute(name, value);
 1809+ }
 1810+ return element;
 1811+ },
 1812+
 1813+ getHeight: function(element) {
 1814+ return $(element).getDimensions().height;
 1815+ },
 1816+
 1817+ getWidth: function(element) {
 1818+ return $(element).getDimensions().width;
 1819+ },
 1820+
 1821+ classNames: function(element) {
 1822+ return new Element.ClassNames(element);
 1823+ },
 1824+
 1825+ hasClassName: function(element, className) {
 1826+ if (!(element = $(element))) return;
 1827+ var elementClassName = element.className;
 1828+ return (elementClassName.length > 0 && (elementClassName == className ||
 1829+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
 1830+ },
 1831+
 1832+ addClassName: function(element, className) {
 1833+ if (!(element = $(element))) return;
 1834+ if (!element.hasClassName(className))
 1835+ element.className += (element.className ? ' ' : '') + className;
 1836+ return element;
 1837+ },
 1838+
 1839+ removeClassName: function(element, className) {
 1840+ if (!(element = $(element))) return;
 1841+ element.className = element.className.replace(
 1842+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
 1843+ return element;
 1844+ },
 1845+
 1846+ toggleClassName: function(element, className) {
 1847+ if (!(element = $(element))) return;
 1848+ return element[element.hasClassName(className) ?
 1849+ 'removeClassName' : 'addClassName'](className);
 1850+ },
 1851+
 1852+ // removes whitespace-only text node children
 1853+ cleanWhitespace: function(element) {
 1854+ element = $(element);
 1855+ var node = element.firstChild;
 1856+ while (node) {
 1857+ var nextNode = node.nextSibling;
 1858+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
 1859+ element.removeChild(node);
 1860+ node = nextNode;
 1861+ }
 1862+ return element;
 1863+ },
 1864+
 1865+ empty: function(element) {
 1866+ return $(element).innerHTML.blank();
 1867+ },
 1868+
 1869+ descendantOf: function(element, ancestor) {
 1870+ element = $(element), ancestor = $(ancestor);
 1871+
 1872+ if (element.compareDocumentPosition)
 1873+ return (element.compareDocumentPosition(ancestor) & 8) === 8;
 1874+
 1875+ if (element.sourceIndex && !Prototype.Browser.Opera) {
 1876+ var e = element.sourceIndex, a = ancestor.sourceIndex,
 1877+ nextAncestor = ancestor.nextSibling;
 1878+ if (!nextAncestor) {
 1879+ do { ancestor = ancestor.parentNode; }
 1880+ while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
 1881+ }
 1882+ if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex);
 1883+ }
 1884+
 1885+ while (element = element.parentNode)
 1886+ if (element == ancestor) return true;
 1887+ return false;
 1888+ },
 1889+
 1890+ scrollTo: function(element) {
 1891+ element = $(element);
 1892+ var pos = element.cumulativeOffset();
 1893+ window.scrollTo(pos[0], pos[1]);
 1894+ return element;
 1895+ },
 1896+
 1897+ getStyle: function(element, style) {
 1898+ element = $(element);
 1899+ style = style == 'float' ? 'cssFloat' : style.camelize();
 1900+ var value = element.style[style];
 1901+ if (!value) {
 1902+ var css = document.defaultView.getComputedStyle(element, null);
 1903+ value = css ? css[style] : null;
 1904+ }
 1905+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
 1906+ return value == 'auto' ? null : value;
 1907+ },
 1908+
 1909+ getOpacity: function(element) {
 1910+ return $(element).getStyle('opacity');
 1911+ },
 1912+
 1913+ setStyle: function(element, styles) {
 1914+ element = $(element);
 1915+ var elementStyle = element.style, match;
 1916+ if (Object.isString(styles)) {
 1917+ element.style.cssText += ';' + styles;
 1918+ return styles.include('opacity') ?
 1919+ element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
 1920+ }
 1921+ for (var property in styles)
 1922+ if (property == 'opacity') element.setOpacity(styles[property]);
 1923+ else
 1924+ elementStyle[(property == 'float' || property == 'cssFloat') ?
 1925+ (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
 1926+ property] = styles[property];
 1927+
 1928+ return element;
 1929+ },
 1930+
 1931+ setOpacity: function(element, value) {
 1932+ element = $(element);
 1933+ element.style.opacity = (value == 1 || value === '') ? '' :
 1934+ (value < 0.00001) ? 0 : value;
 1935+ return element;
 1936+ },
 1937+
 1938+ getDimensions: function(element) {
 1939+ element = $(element);
 1940+ var display = $(element).getStyle('display');
 1941+ if (display != 'none' && display != null) // Safari bug
 1942+ return {width: element.offsetWidth, height: element.offsetHeight};
 1943+
 1944+ // All *Width and *Height properties give 0 on elements with display none,
 1945+ // so enable the element temporarily
 1946+ var els = element.style;
 1947+ var originalVisibility = els.visibility;
 1948+ var originalPosition = els.position;
 1949+ var originalDisplay = els.display;
 1950+ els.visibility = 'hidden';
 1951+ els.position = 'absolute';
 1952+ els.display = 'block';
 1953+ var originalWidth = element.clientWidth;
 1954+ var originalHeight = element.clientHeight;
 1955+ els.display = originalDisplay;
 1956+ els.position = originalPosition;
 1957+ els.visibility = originalVisibility;
 1958+ return {width: originalWidth, height: originalHeight};
 1959+ },
 1960+
 1961+ makePositioned: function(element) {
 1962+ element = $(element);
 1963+ var pos = Element.getStyle(element, 'position');
 1964+ if (pos == 'static' || !pos) {
 1965+ element._madePositioned = true;
 1966+ element.style.position = 'relative';
 1967+ // Opera returns the offset relative to the positioning context, when an
 1968+ // element is position relative but top and left have not been defined
 1969+ if (window.opera) {
 1970+ element.style.top = 0;
 1971+ element.style.left = 0;
 1972+ }
 1973+ }
 1974+ return element;
 1975+ },
 1976+
 1977+ undoPositioned: function(element) {
 1978+ element = $(element);
 1979+ if (element._madePositioned) {
 1980+ element._madePositioned = undefined;
 1981+ element.style.position =
 1982+ element.style.top =
 1983+ element.style.left =
 1984+ element.style.bottom =
 1985+ element.style.right = '';
 1986+ }
 1987+ return element;
 1988+ },
 1989+
 1990+ makeClipping: function(element) {
 1991+ element = $(element);
 1992+ if (element._overflow) return element;
 1993+ element._overflow = Element.getStyle(element, 'overflow') || 'auto';
 1994+ if (element._overflow !== 'hidden')
 1995+ element.style.overflow = 'hidden';
 1996+ return element;
 1997+ },
 1998+
 1999+ undoClipping: function(element) {
 2000+ element = $(element);
 2001+ if (!element._overflow) return element;
 2002+ element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
 2003+ element._overflow = null;
 2004+ return element;
 2005+ },
 2006+
 2007+ cumulativeOffset: function(element) {
 2008+ var valueT = 0, valueL = 0;
 2009+ do {
 2010+ valueT += element.offsetTop || 0;
 2011+ valueL += element.offsetLeft || 0;
 2012+ element = element.offsetParent;
 2013+ } while (element);
 2014+ return Element._returnOffset(valueL, valueT);
 2015+ },
 2016+
 2017+ positionedOffset: function(element) {
 2018+ var valueT = 0, valueL = 0;
 2019+ do {
 2020+ valueT += element.offsetTop || 0;
 2021+ valueL += element.offsetLeft || 0;
 2022+ element = element.offsetParent;
 2023+ if (element) {
 2024+ if (element.tagName == 'BODY') break;
 2025+ var p = Element.getStyle(element, 'position');
 2026+ if (p == 'relative' || p == 'absolute') break;
 2027+ }
 2028+ } while (element);
 2029+ return Element._returnOffset(valueL, valueT);
 2030+ },
 2031+
 2032+ absolutize: function(element) {
 2033+ element = $(element);
 2034+ if (element.getStyle('position') == 'absolute') return;
 2035+ // Position.prepare(); // To be done manually by Scripty when it needs it.
 2036+
 2037+ var offsets = element.positionedOffset();
 2038+ var top = offsets[1];
 2039+ var left = offsets[0];
 2040+ var width = element.clientWidth;
 2041+ var height = element.clientHeight;
 2042+
 2043+ element._originalLeft = left - parseFloat(element.style.left || 0);
 2044+ element._originalTop = top - parseFloat(element.style.top || 0);
 2045+ element._originalWidth = element.style.width;
 2046+ element._originalHeight = element.style.height;
 2047+
 2048+ element.style.position = 'absolute';
 2049+ element.style.top = top + 'px';
 2050+ element.style.left = left + 'px';
 2051+ element.style.width = width + 'px';
 2052+ element.style.height = height + 'px';
 2053+ return element;
 2054+ },
 2055+
 2056+ relativize: function(element) {
 2057+ element = $(element);
 2058+ if (element.getStyle('position') == 'relative') return;
 2059+ // Position.prepare(); // To be done manually by Scripty when it needs it.
 2060+
 2061+ element.style.position = 'relative';
 2062+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
 2063+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
 2064+
 2065+ element.style.top = top + 'px';
 2066+ element.style.left = left + 'px';
 2067+ element.style.height = element._originalHeight;
 2068+ element.style.width = element._originalWidth;
 2069+ return element;
 2070+ },
 2071+
 2072+ cumulativeScrollOffset: function(element) {
 2073+ var valueT = 0, valueL = 0;
 2074+ do {
 2075+ valueT += element.scrollTop || 0;
 2076+ valueL += element.scrollLeft || 0;
 2077+ element = element.parentNode;
 2078+ } while (element);
 2079+ return Element._returnOffset(valueL, valueT);
 2080+ },
 2081+
 2082+ getOffsetParent: function(element) {
 2083+ if (element.offsetParent) return $(element.offsetParent);
 2084+ if (element == document.body) return $(element);
 2085+
 2086+ while ((element = element.parentNode) && element != document.body)
 2087+ if (Element.getStyle(element, 'position') != 'static')
 2088+ return $(element);
 2089+
 2090+ return $(document.body);
 2091+ },
 2092+
 2093+ viewportOffset: function(forElement) {
 2094+ var valueT = 0, valueL = 0;
 2095+
 2096+ var element = forElement;
 2097+ do {
 2098+ valueT += element.offsetTop || 0;
 2099+ valueL += element.offsetLeft || 0;
 2100+
 2101+ // Safari fix
 2102+ if (element.offsetParent == document.body &&
 2103+ Element.getStyle(element, 'position') == 'absolute') break;
 2104+
 2105+ } while (element = element.offsetParent);
 2106+
 2107+ element = forElement;
 2108+ do {
 2109+ if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
 2110+ valueT -= element.scrollTop || 0;
 2111+ valueL -= element.scrollLeft || 0;
 2112+ }
 2113+ } while (element = element.parentNode);
 2114+
 2115+ return Element._returnOffset(valueL, valueT);
 2116+ },
 2117+
 2118+ clonePosition: function(element, source) {
 2119+ var options = Object.extend({
 2120+ setLeft: true,
 2121+ setTop: true,
 2122+ setWidth: true,
 2123+ setHeight: true,
 2124+ offsetTop: 0,
 2125+ offsetLeft: 0
 2126+ }, arguments[2] || { });
 2127+
 2128+ // find page position of source
 2129+ source = $(source);
 2130+ var p = source.viewportOffset();
 2131+
 2132+ // find coordinate system to use
 2133+ element = $(element);
 2134+ var delta = [0, 0];
 2135+ var parent = null;
 2136+ // delta [0,0] will do fine with position: fixed elements,
 2137+ // position:absolute needs offsetParent deltas
 2138+ if (Element.getStyle(element, 'position') == 'absolute') {
 2139+ parent = element.getOffsetParent();
 2140+ delta = parent.viewportOffset();
 2141+ }
 2142+
 2143+ // correct by body offsets (fixes Safari)
 2144+ if (parent == document.body) {
 2145+ delta[0] -= document.body.offsetLeft;
 2146+ delta[1] -= document.body.offsetTop;
 2147+ }
 2148+
 2149+ // set position
 2150+ if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
 2151+ if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
 2152+ if (options.setWidth) element.style.width = source.offsetWidth + 'px';
 2153+ if (options.setHeight) element.style.height = source.offsetHeight + 'px';
 2154+ return element;
 2155+ }
 2156+};
 2157+
 2158+Element.Methods.identify.counter = 1;
 2159+
 2160+Object.extend(Element.Methods, {
 2161+ getElementsBySelector: Element.Methods.select,
 2162+ childElements: Element.Methods.immediateDescendants
 2163+});
 2164+
 2165+Element._attributeTranslations = {
 2166+ write: {
 2167+ names: {
 2168+ className: 'class',
 2169+ htmlFor: 'for'
 2170+ },
 2171+ values: { }
 2172+ }
 2173+};
 2174+
 2175+
 2176+if (!document.createRange || Prototype.Browser.Opera) {
 2177+ Element.Methods.insert = function(element, insertions) {
 2178+ element = $(element);
 2179+
 2180+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
 2181+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
 2182+ insertions = { bottom: insertions };
 2183+
 2184+ var t = Element._insertionTranslations, content, position, pos, tagName;
 2185+
 2186+ for (position in insertions) {
 2187+ content = insertions[position];
 2188+ position = position.toLowerCase();
 2189+ pos = t[position];
 2190+
 2191+ if (content && content.toElement) content = content.toElement();
 2192+ if (Object.isElement(content)) {
 2193+ pos.insert(element, content);
 2194+ continue;
 2195+ }
 2196+
 2197+ content = Object.toHTML(content);
 2198+ tagName = ((position == 'before' || position == 'after')
 2199+ ? element.parentNode : element).tagName.toUpperCase();
 2200+
 2201+ if (t.tags[tagName]) {
 2202+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
 2203+ if (position == 'top' || position == 'after') fragments.reverse();
 2204+ fragments.each(pos.insert.curry(element));
 2205+ }
 2206+ else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
 2207+
 2208+ content.evalScripts.bind(content).defer();
 2209+ }
 2210+
 2211+ return element;
 2212+ };
 2213+}
 2214+
 2215+if (Prototype.Browser.Opera) {
 2216+ Element.Methods._getStyle = Element.Methods.getStyle;
 2217+ Element.Methods.getStyle = function(element, style) {
 2218+ switch(style) {
 2219+ case 'left':
 2220+ case 'top':
 2221+ case 'right':
 2222+ case 'bottom':
 2223+ if (Element._getStyle(element, 'position') == 'static') return null;
 2224+ default: return Element._getStyle(element, style);
 2225+ }
 2226+ };
 2227+ Element.Methods._readAttribute = Element.Methods.readAttribute;
 2228+ Element.Methods.readAttribute = function(element, attribute) {
 2229+ if (attribute == 'title') return element.title;
 2230+ return Element._readAttribute(element, attribute);
 2231+ };
 2232+}
 2233+
 2234+else if (Prototype.Browser.IE) {
 2235+ $w('positionedOffset getOffsetParent viewportOffset').each(function(method) {
 2236+ Element.Methods[method] = Element.Methods[method].wrap(
 2237+ function(proceed, element) {
 2238+ element = $(element);
 2239+ var position = element.getStyle('position');
 2240+ if (position != 'static') return proceed(element);
 2241+ element.setStyle({ position: 'relative' });
 2242+ var value = proceed(element);
 2243+ element.setStyle({ position: position });
 2244+ return value;
 2245+ }
 2246+ );
 2247+ });
 2248+
 2249+ Element.Methods.getStyle = function(element, style) {
 2250+ element = $(element);
 2251+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
 2252+ var value = element.style[style];
 2253+ if (!value && element.currentStyle) value = element.currentStyle[style];
 2254+
 2255+ if (style == 'opacity') {
 2256+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
 2257+ if (value[1]) return parseFloat(value[1]) / 100;
 2258+ return 1.0;
 2259+ }
 2260+
 2261+ if (value == 'auto') {
 2262+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
 2263+ return element['offset' + style.capitalize()] + 'px';
 2264+ return null;
 2265+ }
 2266+ return value;
 2267+ };
 2268+
 2269+ Element.Methods.setOpacity = function(element, value) {
 2270+ function stripAlpha(filter){
 2271+ return filter.replace(/alpha\([^\)]*\)/gi,'');
 2272+ }
 2273+ element = $(element);
 2274+ var currentStyle = element.currentStyle;
 2275+ if ((currentStyle && !currentStyle.hasLayout) ||
 2276+ (!currentStyle && element.style.zoom == 'normal'))
 2277+ element.style.zoom = 1;
 2278+
 2279+ var filter = element.getStyle('filter'), style = element.style;
 2280+ if (value == 1 || value === '') {
 2281+ (filter = stripAlpha(filter)) ?
 2282+ style.filter = filter : style.removeAttribute('filter');
 2283+ return element;
 2284+ } else if (value < 0.00001) value = 0;
 2285+ style.filter = stripAlpha(filter) +
 2286+ 'alpha(opacity=' + (value * 100) + ')';
 2287+ return element;
 2288+ };
 2289+
 2290+ Element._attributeTranslations = {
 2291+ read: {
 2292+ names: {
 2293+ 'class': 'className',
 2294+ 'for': 'htmlFor'
 2295+ },
 2296+ values: {
 2297+ _getAttr: function(element, attribute) {
 2298+ return element.getAttribute(attribute, 2);
 2299+ },
 2300+ _getAttrNode: function(element, attribute) {
 2301+ var node = element.getAttributeNode(attribute);
 2302+ return node ? node.value : "";
 2303+ },
 2304+ _getEv: function(element, attribute) {
 2305+ var attribute = element.getAttribute(attribute);
 2306+ return attribute ? attribute.toString().slice(23, -2) : null;
 2307+ },
 2308+ _flag: function(element, attribute) {
 2309+ return $(element).hasAttribute(attribute) ? attribute : null;
 2310+ },
 2311+ style: function(element) {
 2312+ return element.style.cssText.toLowerCase();
 2313+ },
 2314+ title: function(element) {
 2315+ return element.title;
 2316+ }
 2317+ }
 2318+ }
 2319+ };
 2320+
 2321+ Element._attributeTranslations.write = {
 2322+ names: Object.clone(Element._attributeTranslations.read.names),
 2323+ values: {
 2324+ checked: function(element, value) {
 2325+ element.checked = !!value;
 2326+ },
 2327+
 2328+ style: function(element, value) {
 2329+ element.style.cssText = value ? value : '';
 2330+ }
 2331+ }
 2332+ };
 2333+
 2334+ Element._attributeTranslations.has = {};
 2335+
 2336+ $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
 2337+ 'encType maxLength readOnly longDesc').each(function(attr) {
 2338+ Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
 2339+ Element._attributeTranslations.has[attr.toLowerCase()] = attr;
 2340+ });
 2341+
 2342+ (function(v) {
 2343+ Object.extend(v, {
 2344+ href: v._getAttr,
 2345+ src: v._getAttr,
 2346+ type: v._getAttr,
 2347+ action: v._getAttrNode,
 2348+ disabled: v._flag,
 2349+ checked: v._flag,
 2350+ readonly: v._flag,
 2351+ multiple: v._flag,
 2352+ onload: v._getEv,
 2353+ onunload: v._getEv,
 2354+ onclick: v._getEv,
 2355+ ondblclick: v._getEv,
 2356+ onmousedown: v._getEv,
 2357+ onmouseup: v._getEv,
 2358+ onmouseover: v._getEv,
 2359+ onmousemove: v._getEv,
 2360+ onmouseout: v._getEv,
 2361+ onfocus: v._getEv,
 2362+ onblur: v._getEv,
 2363+ onkeypress: v._getEv,
 2364+ onkeydown: v._getEv,
 2365+ onkeyup: v._getEv,
 2366+ onsubmit: v._getEv,
 2367+ onreset: v._getEv,
 2368+ onselect: v._getEv,
 2369+ onchange: v._getEv
 2370+ });
 2371+ })(Element._attributeTranslations.read.values);
 2372+}
 2373+
 2374+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
 2375+ Element.Methods.setOpacity = function(element, value) {
 2376+ element = $(element);
 2377+ element.style.opacity = (value == 1) ? 0.999999 :
 2378+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
 2379+ return element;
 2380+ };
 2381+}
 2382+
 2383+else if (Prototype.Browser.WebKit) {
 2384+ Element.Methods.setOpacity = function(element, value) {
 2385+ element = $(element);
 2386+ element.style.opacity = (value == 1 || value === '') ? '' :
 2387+ (value < 0.00001) ? 0 : value;
 2388+
 2389+ if (value == 1)
 2390+ if(element.tagName == 'IMG' && element.width) {
 2391+ element.width++; element.width--;
 2392+ } else try {
 2393+ var n = document.createTextNode(' ');
 2394+ element.appendChild(n);
 2395+ element.removeChild(n);
 2396+ } catch (e) { }
 2397+
 2398+ return element;
 2399+ };
 2400+
 2401+ // Safari returns margins on body which is incorrect if the child is absolutely
 2402+ // positioned. For performance reasons, redefine Position.cumulativeOffset for
 2403+ // KHTML/WebKit only.
 2404+ Element.Methods.cumulativeOffset = function(element) {
 2405+ var valueT = 0, valueL = 0;
 2406+ do {
 2407+ valueT += element.offsetTop || 0;
 2408+ valueL += element.offsetLeft || 0;
 2409+ if (element.offsetParent == document.body)
 2410+ if (Element.getStyle(element, 'position') == 'absolute') break;
 2411+
 2412+ element = element.offsetParent;
 2413+ } while (element);
 2414+
 2415+ return Element._returnOffset(valueL, valueT);
 2416+ };
 2417+}
 2418+
 2419+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
 2420+ // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
 2421+ Element.Methods.update = function(element, content) {
 2422+ element = $(element);
 2423+
 2424+ if (content && content.toElement) content = content.toElement();
 2425+ if (Object.isElement(content)) return element.update().insert(content);
 2426+
 2427+ content = Object.toHTML(content);
 2428+ var tagName = element.tagName.toUpperCase();
 2429+
 2430+ if (tagName in Element._insertionTranslations.tags) {
 2431+ $A(element.childNodes).each(function(node) { element.removeChild(node) });
 2432+ Element._getContentFromAnonymousElement(tagName, content.stripScripts())
 2433+ .each(function(node) { element.appendChild(node) });
 2434+ }
 2435+ else element.innerHTML = content.stripScripts();
 2436+
 2437+ content.evalScripts.bind(content).defer();
 2438+ return element;
 2439+ };
 2440+}
 2441+
 2442+if (document.createElement('div').outerHTML) {
 2443+ Element.Methods.replace = function(element, content) {
 2444+ element = $(element);
 2445+
 2446+ if (content && content.toElement) content = content.toElement();
 2447+ if (Object.isElement(content)) {
 2448+ element.parentNode.replaceChild(content, element);
 2449+ return element;
 2450+ }
 2451+
 2452+ content = Object.toHTML(content);
 2453+ var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
 2454+
 2455+ if (Element._insertionTranslations.tags[tagName]) {
 2456+ var nextSibling = element.next();
 2457+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
 2458+ parent.removeChild(element);
 2459+ if (nextSibling)
 2460+ fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
 2461+ else
 2462+ fragments.each(function(node) { parent.appendChild(node) });
 2463+ }
 2464+ else element.outerHTML = content.stripScripts();
 2465+
 2466+ content.evalScripts.bind(content).defer();
 2467+ return element;
 2468+ };
 2469+}
 2470+
 2471+Element._returnOffset = function(l, t) {
 2472+ var result = [l, t];
 2473+ result.left = l;
 2474+ result.top = t;
 2475+ return result;
 2476+};
 2477+
 2478+Element._getContentFromAnonymousElement = function(tagName, html) {
 2479+ var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
 2480+ div.innerHTML = t[0] + html + t[1];
 2481+ t[2].times(function() { div = div.firstChild });
 2482+ return $A(div.childNodes);
 2483+};
 2484+
 2485+Element._insertionTranslations = {
 2486+ before: {
 2487+ adjacency: 'beforeBegin',
 2488+ insert: function(element, node) {
 2489+ element.parentNode.insertBefore(node, element);
 2490+ },
 2491+ initializeRange: function(element, range) {
 2492+ range.setStartBefore(element);
 2493+ }
 2494+ },
 2495+ top: {
 2496+ adjacency: 'afterBegin',
 2497+ insert: function(element, node) {
 2498+ element.insertBefore(node, element.firstChild);
 2499+ },
 2500+ initializeRange: function(element, range) {
 2501+ range.selectNodeContents(element);
 2502+ range.collapse(true);
 2503+ }
 2504+ },
 2505+ bottom: {
 2506+ adjacency: 'beforeEnd',
 2507+ insert: function(element, node) {
 2508+ element.appendChild(node);
 2509+ }
 2510+ },
 2511+ after: {
 2512+ adjacency: 'afterEnd',
 2513+ insert: function(element, node) {
 2514+ element.parentNode.insertBefore(node, element.nextSibling);
 2515+ },
 2516+ initializeRange: function(element, range) {
 2517+ range.setStartAfter(element);
 2518+ }
 2519+ },
 2520+ tags: {
 2521+ TABLE: ['<table>', '</table>', 1],
 2522+ TBODY: ['<table><tbody>', '</tbody></table>', 2],
 2523+ TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
 2524+ TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
 2525+ SELECT: ['<select>', '</select>', 1]
 2526+ }
 2527+};
 2528+
 2529+(function() {
 2530+ this.bottom.initializeRange = this.top.initializeRange;
 2531+ Object.extend(this.tags, {
 2532+ THEAD: this.tags.TBODY,
 2533+ TFOOT: this.tags.TBODY,
 2534+ TH: this.tags.TD
 2535+ });
 2536+}).call(Element._insertionTranslations);
 2537+
 2538+Element.Methods.Simulated = {
 2539+ hasAttribute: function(element, attribute) {
 2540+ attribute = Element._attributeTranslations.has[attribute] || attribute;
 2541+ var node = $(element).getAttributeNode(attribute);
 2542+ return node && node.specified;
 2543+ }
 2544+};
 2545+
 2546+Element.Methods.ByTag = { };
 2547+
 2548+Object.extend(Element, Element.Methods);
 2549+
 2550+if (!Prototype.BrowserFeatures.ElementExtensions &&
 2551+ document.createElement('div').__proto__) {
 2552+ window.HTMLElement = { };
 2553+ window.HTMLElement.prototype = document.createElement('div').__proto__;
 2554+ Prototype.BrowserFeatures.ElementExtensions = true;
 2555+}
 2556+
 2557+Element.extend = (function() {
 2558+ if (Prototype.BrowserFeatures.SpecificElementExtensions)
 2559+ return Prototype.K;
 2560+
 2561+ var Methods = { }, ByTag = Element.Methods.ByTag;
 2562+
 2563+ var extend = Object.extend(function(element) {
 2564+ if (!element || element._extendedByPrototype ||
 2565+ element.nodeType != 1 || element == window) return element;
 2566+
 2567+ var methods = Object.clone(Methods),
 2568+ tagName = element.tagName, property, value;
 2569+
 2570+ // extend methods for specific tags
 2571+ if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
 2572+
 2573+ for (property in methods) {
 2574+ value = methods[property];
 2575+ if (Object.isFunction(value) && !(property in element))
 2576+ element[property] = value.methodize();
 2577+ }
 2578+
 2579+ element._extendedByPrototype = Prototype.emptyFunction;
 2580+ return element;
 2581+
 2582+ }, {
 2583+ refresh: function() {
 2584+ // extend methods for all tags (Safari doesn't need this)
 2585+ if (!Prototype.BrowserFeatures.ElementExtensions) {
 2586+ Object.extend(Methods, Element.Methods);
 2587+ Object.extend(Methods, Element.Methods.Simulated);
 2588+ }
 2589+ }
 2590+ });
 2591+
 2592+ extend.refresh();
 2593+ return extend;
 2594+})();
 2595+
 2596+Element.hasAttribute = function(element, attribute) {
 2597+ if (element.hasAttribute) return element.hasAttribute(attribute);
 2598+ return Element.Methods.Simulated.hasAttribute(element, attribute);
 2599+};
 2600+
 2601+Element.addMethods = function(methods) {
 2602+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
 2603+
 2604+ if (!methods) {
 2605+ Object.extend(Form, Form.Methods);
 2606+ Object.extend(Form.Element, Form.Element.Methods);
 2607+ Object.extend(Element.Methods.ByTag, {
 2608+ "FORM": Object.clone(Form.Methods),
 2609+ "INPUT": Object.clone(Form.Element.Methods),
 2610+ "SELECT": Object.clone(Form.Element.Methods),
 2611+ "TEXTAREA": Object.clone(Form.Element.Methods)
 2612+ });
 2613+ }
 2614+
 2615+ if (arguments.length == 2) {
 2616+ var tagName = methods;
 2617+ methods = arguments[1];
 2618+ }
 2619+
 2620+ if (!tagName) Object.extend(Element.Methods, methods || { });
 2621+ else {
 2622+ if (Object.isArray(tagName)) tagName.each(extend);
 2623+ else extend(tagName);
 2624+ }
 2625+
 2626+ function extend(tagName) {
 2627+ tagName = tagName.toUpperCase();
 2628+ if (!Element.Methods.ByTag[tagName])
 2629+ Element.Methods.ByTag[tagName] = { };
 2630+ Object.extend(Element.Methods.ByTag[tagName], methods);
 2631+ }
 2632+
 2633+ function copy(methods, destination, onlyIfAbsent) {
 2634+ onlyIfAbsent = onlyIfAbsent || false;
 2635+ for (var property in methods) {
 2636+ var value = methods[property];
 2637+ if (!Object.isFunction(value)) continue;
 2638+ if (!onlyIfAbsent || !(property in destination))
 2639+ destination[property] = value.methodize();
 2640+ }
 2641+ }
 2642+
 2643+ function findDOMClass(tagName) {
 2644+ var klass;
 2645+ var trans = {
 2646+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
 2647+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
 2648+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
 2649+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
 2650+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
 2651+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
 2652+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
 2653+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
 2654+ "FrameSet", "IFRAME": "IFrame"
 2655+ };
 2656+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
 2657+ if (window[klass]) return window[klass];
 2658+ klass = 'HTML' + tagName + 'Element';
 2659+ if (window[klass]) return window[klass];
 2660+ klass = 'HTML' + tagName.capitalize() + 'Element';
 2661+ if (window[klass]) return window[klass];
 2662+
 2663+ window[klass] = { };
 2664+ window[klass].prototype = document.createElement(tagName).__proto__;
 2665+ return window[klass];
 2666+ }
 2667+
 2668+ if (F.ElementExtensions) {
 2669+ copy(Element.Methods, HTMLElement.prototype);
 2670+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
 2671+ }
 2672+
 2673+ if (F.SpecificElementExtensions) {
 2674+ for (var tag in Element.Methods.ByTag) {
 2675+ var klass = findDOMClass(tag);
 2676+ if (Object.isUndefined(klass)) continue;
 2677+ copy(T[tag], klass.prototype);
 2678+ }
 2679+ }
 2680+
 2681+ Object.extend(Element, Element.Methods);
 2682+ delete Element.ByTag;
 2683+
 2684+ if (Element.extend.refresh) Element.extend.refresh();
 2685+ Element.cache = { };
 2686+};
 2687+
 2688+document.viewport = {
 2689+ getDimensions: function() {
 2690+ var dimensions = { };
 2691+ $w('width height').each(function(d) {
 2692+ var D = d.capitalize();
 2693+ dimensions[d] = self['inner' + D] ||
 2694+ (document.documentElement['client' + D] || document.body['client' + D]);
 2695+ });
 2696+ return dimensions;
 2697+ },
 2698+
 2699+ getWidth: function() {
 2700+ return this.getDimensions().width;
 2701+ },
 2702+
 2703+ getHeight: function() {
 2704+ return this.getDimensions().height;
 2705+ },
 2706+
 2707+ getScrollOffsets: function() {
 2708+ return Element._returnOffset(
 2709+ window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
 2710+ window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
 2711+ }
 2712+};
 2713+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
 2714+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 2715+ * license. Please see http://www.yui-ext.com/ for more information. */
 2716+
 2717+var Selector = Class.create({
 2718+ initialize: function(expression) {
 2719+ this.expression = expression.strip();
 2720+ this.compileMatcher();
 2721+ },
 2722+
 2723+ compileMatcher: function() {
 2724+ // Selectors with namespaced attributes can't use the XPath version
 2725+ if (Prototype.BrowserFeatures.XPath && !(/(\[[\w-]*?:|:checked)/).test(this.expression))
 2726+ return this.compileXPathMatcher();
 2727+
 2728+ var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
 2729+ c = Selector.criteria, le, p, m;
 2730+
 2731+ if (Selector._cache[e]) {
 2732+ this.matcher = Selector._cache[e];
 2733+ return;
 2734+ }
 2735+
 2736+ this.matcher = ["this.matcher = function(root) {",
 2737+ "var r = root, h = Selector.handlers, c = false, n;"];
 2738+
 2739+ while (e && le != e && (/\S/).test(e)) {
 2740+ le = e;
 2741+ for (var i in ps) {
 2742+ p = ps[i];
 2743+ if (m = e.match(p)) {
 2744+ this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
 2745+ new Template(c[i]).evaluate(m));
 2746+ e = e.replace(m[0], '');
 2747+ break;
 2748+ }
 2749+ }
 2750+ }
 2751+
 2752+ this.matcher.push("return h.unique(n);\n}");
 2753+ eval(this.matcher.join('\n'));
 2754+ Selector._cache[this.expression] = this.matcher;
 2755+ },
 2756+
 2757+ compileXPathMatcher: function() {
 2758+ var e = this.expression, ps = Selector.patterns,
 2759+ x = Selector.xpath, le, m;
 2760+
 2761+ if (Selector._cache[e]) {
 2762+ this.xpath = Selector._cache[e]; return;
 2763+ }
 2764+
 2765+ this.matcher = ['.//*'];
 2766+ while (e && le != e && (/\S/).test(e)) {
 2767+ le = e;
 2768+ for (var i in ps) {
 2769+ if (m = e.match(ps[i])) {
 2770+ this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
 2771+ new Template(x[i]).evaluate(m));
 2772+ e = e.replace(m[0], '');
 2773+ break;
 2774+ }
 2775+ }
 2776+ }
 2777+
 2778+ this.xpath = this.matcher.join('');
 2779+ Selector._cache[this.expression] = this.xpath;
 2780+ },
 2781+
 2782+ findElements: function(root) {
 2783+ root = root || document;
 2784+ if (this.xpath) return document._getElementsByXPath(this.xpath, root);
 2785+ return this.matcher(root);
 2786+ },
 2787+
 2788+ match: function(element) {
 2789+ this.tokens = [];
 2790+
 2791+ var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
 2792+ var le, p, m;
 2793+
 2794+ while (e && le !== e && (/\S/).test(e)) {
 2795+ le = e;
 2796+ for (var i in ps) {
 2797+ p = ps[i];
 2798+ if (m = e.match(p)) {
 2799+ // use the Selector.assertions methods unless the selector
 2800+ // is too complex.
 2801+ if (as[i]) {
 2802+ this.tokens.push([i, Object.clone(m)]);
 2803+ e = e.replace(m[0], '');
 2804+ } else {
 2805+ // reluctantly do a document-wide search
 2806+ // and look for a match in the array
 2807+ return this.findElements(document).include(element);
 2808+ }
 2809+ }
 2810+ }
 2811+ }
 2812+
 2813+ var match = true, name, matches;
 2814+ for (var i = 0, token; token = this.tokens[i]; i++) {
 2815+ name = token[0], matches = token[1];
 2816+ if (!Selector.assertions[name](element, matches)) {
 2817+ match = false; break;
 2818+ }
 2819+ }
 2820+
 2821+ return match;
 2822+ },
 2823+
 2824+ toString: function() {
 2825+ return this.expression;
 2826+ },
 2827+
 2828+ inspect: function() {
 2829+ return "#<Selector:" + this.expression.inspect() + ">";
 2830+ }
 2831+});
 2832+
 2833+Object.extend(Selector, {
 2834+ _cache: { },
 2835+
 2836+ xpath: {
 2837+ descendant: "//*",
 2838+ child: "/*",
 2839+ adjacent: "/following-sibling::*[1]",
 2840+ laterSibling: '/following-sibling::*',
 2841+ tagName: function(m) {
 2842+ if (m[1] == '*') return '';
 2843+ return "[local-name()='" + m[1].toLowerCase() +
 2844+ "' or local-name()='" + m[1].toUpperCase() + "']";
 2845+ },
 2846+ className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
 2847+ id: "[@id='#{1}']",
 2848+ attrPresence: "[@#{1}]",
 2849+ attr: function(m) {
 2850+ m[3] = m[5] || m[6];
 2851+ return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
 2852+ },
 2853+ pseudo: function(m) {
 2854+ var h = Selector.xpath.pseudos[m[1]];
 2855+ if (!h) return '';
 2856+ if (Object.isFunction(h)) return h(m);
 2857+ return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
 2858+ },
 2859+ operators: {
 2860+ '=': "[@#{1}='#{3}']",
 2861+ '!=': "[@#{1}!='#{3}']",
 2862+ '^=': "[starts-with(@#{1}, '#{3}')]",
 2863+ '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
 2864+ '*=': "[contains(@#{1}, '#{3}')]",
 2865+ '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
 2866+ '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
 2867+ },
 2868+ pseudos: {
 2869+ 'first-child': '[not(preceding-sibling::*)]',
 2870+ 'last-child': '[not(following-sibling::*)]',
 2871+ 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
 2872+ 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
 2873+ 'checked': "[@checked]",
 2874+ 'disabled': "[@disabled]",
 2875+ 'enabled': "[not(@disabled)]",
 2876+ 'not': function(m) {
 2877+ var e = m[6], p = Selector.patterns,
 2878+ x = Selector.xpath, le, m, v;
 2879+
 2880+ var exclusion = [];
 2881+ while (e && le != e && (/\S/).test(e)) {
 2882+ le = e;
 2883+ for (var i in p) {
 2884+ if (m = e.match(p[i])) {
 2885+ v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
 2886+ exclusion.push("(" + v.substring(1, v.length - 1) + ")");
 2887+ e = e.replace(m[0], '');
 2888+ break;
 2889+ }
 2890+ }
 2891+ }
 2892+ return "[not(" + exclusion.join(" and ") + ")]";
 2893+ },
 2894+ 'nth-child': function(m) {
 2895+ return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
 2896+ },
 2897+ 'nth-last-child': function(m) {
 2898+ return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
 2899+ },
 2900+ 'nth-of-type': function(m) {
 2901+ return Selector.xpath.pseudos.nth("position() ", m);
 2902+ },
 2903+ 'nth-last-of-type': function(m) {
 2904+ return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
 2905+ },
 2906+ 'first-of-type': function(m) {
 2907+ m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
 2908+ },
 2909+ 'last-of-type': function(m) {
 2910+ m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
 2911+ },
 2912+ 'only-of-type': function(m) {
 2913+ var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
 2914+ },
 2915+ nth: function(fragment, m) {
 2916+ var mm, formula = m[6], predicate;
 2917+ if (formula == 'even') formula = '2n+0';
 2918+ if (formula == 'odd') formula = '2n+1';
 2919+ if (mm = formula.match(/^(\d+)$/)) // digit only
 2920+ return '[' + fragment + "= " + mm[1] + ']';
 2921+ if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
 2922+ if (mm[1] == "-") mm[1] = -1;
 2923+ var a = mm[1] ? Number(mm[1]) : 1;
 2924+ var b = mm[2] ? Number(mm[2]) : 0;
 2925+ predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
 2926+ "((#{fragment} - #{b}) div #{a} >= 0)]";
 2927+ return new Template(predicate).evaluate({
 2928+ fragment: fragment, a: a, b: b });
 2929+ }
 2930+ }
 2931+ }
 2932+ },
 2933+
 2934+ criteria: {
 2935+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
 2936+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
 2937+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
 2938+ attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
 2939+ attr: function(m) {
 2940+ m[3] = (m[5] || m[6]);
 2941+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
 2942+ },
 2943+ pseudo: function(m) {
 2944+ if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
 2945+ return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
 2946+ },
 2947+ descendant: 'c = "descendant";',
 2948+ child: 'c = "child";',
 2949+ adjacent: 'c = "adjacent";',
 2950+ laterSibling: 'c = "laterSibling";'
 2951+ },
 2952+
 2953+ patterns: {
 2954+ // combinators must be listed first
 2955+ // (and descendant needs to be last combinator)
 2956+ laterSibling: /^\s*~\s*/,
 2957+ child: /^\s*>\s*/,
 2958+ adjacent: /^\s*\+\s*/,
 2959+ descendant: /^\s/,
 2960+
 2961+ // selectors follow
 2962+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
 2963+ id: /^#([\w\-\*]+)(\b|$)/,
 2964+ className: /^\.([\w\-\*]+)(\b|$)/,
 2965+ pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/,
 2966+ attrPresence: /^\[([\w]+)\]/,
 2967+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
 2968+ },
 2969+
 2970+ // for Selector.match and Element#match
 2971+ assertions: {
 2972+ tagName: function(element, matches) {
 2973+ return matches[1].toUpperCase() == element.tagName.toUpperCase();
 2974+ },
 2975+
 2976+ className: function(element, matches) {
 2977+ return Element.hasClassName(element, matches[1]);
 2978+ },
 2979+
 2980+ id: function(element, matches) {
 2981+ return element.id === matches[1];
 2982+ },
 2983+
 2984+ attrPresence: function(element, matches) {
 2985+ return Element.hasAttribute(element, matches[1]);
 2986+ },
 2987+
 2988+ attr: function(element, matches) {
 2989+ var nodeValue = Element.readAttribute(element, matches[1]);
 2990+ return Selector.operators[matches[2]](nodeValue, matches[3]);
 2991+ }
 2992+ },
 2993+
 2994+ handlers: {
 2995+ // UTILITY FUNCTIONS
 2996+ // joins two collections
 2997+ concat: function(a, b) {
 2998+ for (var i = 0, node; node = b[i]; i++)
 2999+ a.push(node);
 3000+ return a;
 3001+ },
 3002+
 3003+ // marks an array of nodes for counting
 3004+ mark: function(nodes) {
 3005+ for (var i = 0, node; node = nodes[i]; i++)
 3006+ node._counted = true;
 3007+ return nodes;
 3008+ },
 3009+
 3010+ unmark: function(nodes) {
 3011+ for (var i = 0, node; node = nodes[i]; i++)
 3012+ node._counted = undefined;
 3013+ return nodes;
 3014+ },
 3015+
 3016+ // mark each child node with its position (for nth calls)
 3017+ // "ofType" flag indicates whether we're indexing for nth-of-type
 3018+ // rather than nth-child
 3019+ index: function(parentNode, reverse, ofType) {
 3020+ parentNode._counted = true;
 3021+ if (reverse) {
 3022+ for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
 3023+ var node = nodes[i];
 3024+ if (node.nodeType == 1 && (!ofType || (node._counted==true))) node.nodeIndex = j++;
 3025+ }
 3026+ } else {
 3027+ for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
 3028+ if (node.nodeType == 1 && (!ofType || (node._counted==true))) node.nodeIndex = j++;
 3029+ }
 3030+ },
 3031+
 3032+ // filters out duplicates and extends all nodes
 3033+ unique: function(nodes) {
 3034+ if (nodes.length == 0) return nodes;
 3035+ var results = [], n;
 3036+ for (var i = 0, l = nodes.length; i < l; i++)
 3037+ if ((n = nodes[i])._counted != true) {
 3038+ n._counted = true;
 3039+ results.push(Element.extend(n));
 3040+ }
 3041+ return Selector.handlers.unmark(results);
 3042+ },
 3043+
 3044+ // COMBINATOR FUNCTIONS
 3045+ descendant: function(nodes) {
 3046+ var h = Selector.handlers;
 3047+ for (var i = 0, results = [], node; node = nodes[i]; i++)
 3048+ h.concat(results, node.getElementsByTagName('*'));
 3049+ return results;
 3050+ },
 3051+
 3052+ child: function(nodes) {
 3053+ var h = Selector.handlers;
 3054+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
 3055+ for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
 3056+ if (child.nodeType == 1 && child.tagName != '!') results.push(child);
 3057+ }
 3058+ return results;
 3059+ },
 3060+
 3061+ adjacent: function(nodes) {
 3062+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
 3063+ var next = this.nextElementSibling(node);
 3064+ if (next) results.push(next);
 3065+ }
 3066+ return results;
 3067+ },
 3068+
 3069+ laterSibling: function(nodes) {
 3070+ var h = Selector.handlers;
 3071+ for (var i = 0, results = [], node; node = nodes[i]; i++)
 3072+ h.concat(results, Element.nextSiblings(node));
 3073+ return results;
 3074+ },
 3075+
 3076+ nextElementSibling: function(node) {
 3077+ while (node = node.nextSibling)
 3078+ if (node.nodeType == 1) return node;
 3079+ return null;
 3080+ },
 3081+
 3082+ previousElementSibling: function(node) {
 3083+ while (node = node.previousSibling)
 3084+ if (node.nodeType == 1) return node;
 3085+ return null;
 3086+ },
 3087+
 3088+ // TOKEN FUNCTIONS
 3089+ tagName: function(nodes, root, tagName, combinator) {
 3090+ tagName = tagName.toUpperCase();
 3091+ var results = [], h = Selector.handlers;
 3092+ if (nodes) {
 3093+ if (combinator) {
 3094+ // fastlane for ordinary descendant combinators
 3095+ if (combinator == "descendant") {
 3096+ for (var i = 0, node; node = nodes[i]; i++)
 3097+ h.concat(results, node.getElementsByTagName(tagName));
 3098+ return results;
 3099+ } else nodes = this[combinator](nodes);
 3100+ if (tagName == "*") return nodes;
 3101+ }
 3102+ for (var i = 0, node; node = nodes[i]; i++)
 3103+ if (node.tagName.toUpperCase() == tagName) results.push(node);
 3104+ return results;
 3105+ } else return root.getElementsByTagName(tagName);
 3106+ },
 3107+
 3108+ id: function(nodes, root, id, combinator) {
 3109+ var targetNode = $(id), h = Selector.handlers;
 3110+ if (!targetNode) return [];
 3111+ if (!nodes && root == document) return [targetNode];
 3112+ if (nodes) {
 3113+ if (combinator) {
 3114+ if (combinator == 'child') {
 3115+ for (var i = 0, node; node = nodes[i]; i++)
 3116+ if (targetNode.parentNode == node) return [targetNode];
 3117+ } else if (combinator == 'descendant') {
 3118+ for (var i = 0, node; node = nodes[i]; i++)
 3119+ if (Element.descendantOf(targetNode, node)) return [targetNode];
 3120+ } else if (combinator == 'adjacent') {
 3121+ for (var i = 0, node; node = nodes[i]; i++)
 3122+ if (Selector.handlers.previousElementSibling(targetNode) == node)
 3123+ return [targetNode];
 3124+ } else nodes = h[combinator](nodes);
 3125+ }
 3126+ for (var i = 0, node; node = nodes[i]; i++)
 3127+ if (node == targetNode) return [targetNode];
 3128+ return [];
 3129+ }
 3130+ return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
 3131+ },
 3132+
 3133+ className: function(nodes, root, className, combinator) {
 3134+ if (nodes && combinator) nodes = this[combinator](nodes);
 3135+ return Selector.handlers.byClassName(nodes, root, className);
 3136+ },
 3137+
 3138+ byClassName: function(nodes, root, className) {
 3139+ if (!nodes) nodes = Selector.handlers.descendant([root]);
 3140+ var needle = ' ' + className + ' ';
 3141+ for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
 3142+ nodeClassName = node.className;
 3143+ if (nodeClassName.length == 0) continue;
 3144+ if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
 3145+ results.push(node);
 3146+ }
 3147+ return results;
 3148+ },
 3149+
 3150+ attrPresence: function(nodes, root, attr) {
 3151+ if (!nodes) nodes = root.getElementsByTagName("*");
 3152+ var results = [];
 3153+ for (var i = 0, node; node = nodes[i]; i++)
 3154+ if (Element.hasAttribute(node, attr)) results.push(node);
 3155+ return results;
 3156+ },
 3157+
 3158+ attr: function(nodes, root, attr, value, operator) {
 3159+ if (!nodes) nodes = root.getElementsByTagName("*");
 3160+ var handler = Selector.operators[operator], results = [];
 3161+ for (var i = 0, node; node = nodes[i]; i++) {
 3162+ var nodeValue = Element.readAttribute(node, attr);
 3163+ if (nodeValue === null) continue;
 3164+ if (handler(nodeValue, value)) results.push(node);
 3165+ }
 3166+ return results;
 3167+ },
 3168+
 3169+ pseudo: function(nodes, name, value, root, combinator) {
 3170+ if (nodes && combinator) nodes = this[combinator](nodes);
 3171+ if (!nodes) nodes = root.getElementsByTagName("*");
 3172+ return Selector.pseudos[name](nodes, value, root);
 3173+ }
 3174+ },
 3175+
 3176+ pseudos: {
 3177+ 'first-child': function(nodes, value, root) {
 3178+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
 3179+ if (Selector.handlers.previousElementSibling(node)) continue;
 3180+ results.push(node);
 3181+ }
 3182+ return results;
 3183+ },
 3184+ 'last-child': function(nodes, value, root) {
 3185+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
 3186+ if (Selector.handlers.nextElementSibling(node)) continue;
 3187+ results.push(node);
 3188+ }
 3189+ return results;
 3190+ },
 3191+ 'only-child': function(nodes, value, root) {
 3192+ var h = Selector.handlers;
 3193+ for (var i = 0, results = [], node; node = nodes[i]; i++)
 3194+ if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
 3195+ results.push(node);
 3196+ return results;
 3197+ },
 3198+ 'nth-child': function(nodes, formula, root) {
 3199+ return Selector.pseudos.nth(nodes, formula, root);
 3200+ },
 3201+ 'nth-last-child': function(nodes, formula, root) {
 3202+ return Selector.pseudos.nth(nodes, formula, root, true);
 3203+ },
 3204+ 'nth-of-type': function(nodes, formula, root) {
 3205+ return Selector.pseudos.nth(nodes, formula, root, false, true);
 3206+ },
 3207+ 'nth-last-of-type': function(nodes, formula, root) {
 3208+ return Selector.pseudos.nth(nodes, formula, root, true, true);
 3209+ },
 3210+ 'first-of-type': function(nodes, formula, root) {
 3211+ return Selector.pseudos.nth(nodes, "1", root, false, true);
 3212+ },
 3213+ 'last-of-type': function(nodes, formula, root) {
 3214+ return Selector.pseudos.nth(nodes, "1", root, true, true);
 3215+ },
 3216+ 'only-of-type': function(nodes, formula, root) {
 3217+ var p = Selector.pseudos;
 3218+ return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
 3219+ },
 3220+
 3221+ // handles the an+b logic
 3222+ getIndices: function(a, b, total) {
 3223+ if (a == 0) return b > 0 ? [b] : [];
 3224+ return $R(1, total).inject([], function(memo, i) {
 3225+ if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
 3226+ return memo;
 3227+ });
 3228+ },
 3229+
 3230+ // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
 3231+ nth: function(nodes, formula, root, reverse, ofType) {
 3232+ if (nodes.length == 0) return [];
 3233+ if (formula == 'even') formula = '2n+0';
 3234+ if (formula == 'odd') formula = '2n+1';
 3235+ var h = Selector.handlers, results = [], indexed = [], m;
 3236+ h.mark(nodes);
 3237+ for (var i = 0, node; node = nodes[i]; i++) {
 3238+ if (node.parentNode._counted != true) {
 3239+ h.index(node.parentNode, reverse, ofType);
 3240+ indexed.push(node.parentNode);
 3241+ }
 3242+ }
 3243+ if (formula.match(/^\d+$/)) { // just a number
 3244+ formula = Number(formula);
 3245+ for (var i = 0, node; node = nodes[i]; i++)
 3246+ if (node.nodeIndex == formula) results.push(node);
 3247+ } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
 3248+ if (m[1] == "-") m[1] = -1;
 3249+ var a = m[1] ? Number(m[1]) : 1;
 3250+ var b = m[2] ? Number(m[2]) : 0;
 3251+ var indices = Selector.pseudos.getIndices(a, b, nodes.length);
 3252+ for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
 3253+ for (var j = 0; j < l; j++)
 3254+ if (node.nodeIndex == indices[j]) results.push(node);
 3255+ }
 3256+ }
 3257+ h.unmark(nodes);
 3258+ h.unmark(indexed);
 3259+ return results;
 3260+ },
 3261+
 3262+ 'empty': function(nodes, value, root) {
 3263+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
 3264+ // IE treats comments as element nodes
 3265+ if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
 3266+ results.push(node);
 3267+ }
 3268+ return results;
 3269+ },
 3270+
 3271+ 'not': function(nodes, selector, root) {
 3272+ var h = Selector.handlers, selectorType, m;
 3273+ var exclusions = new Selector(selector).findElements(root);
 3274+ h.mark(exclusions);
 3275+ for (var i = 0, results = [], node; node = nodes[i]; i++)
 3276+ if (node._counted!=true) results.push(node);
 3277+ h.unmark(exclusions);
 3278+ return results;
 3279+ },
 3280+
 3281+ 'enabled': function(nodes, value, root) {
 3282+ for (var i = 0, results = [], node; node = nodes[i]; i++)
 3283+ if (!node.disabled) results.push(node);
 3284+ return results;
 3285+ },
 3286+
 3287+ 'disabled': function(nodes, value, root) {
 3288+ for (var i = 0, results = [], node; node = nodes[i]; i++)
 3289+ if (node.disabled) results.push(node);
 3290+ return results;
 3291+ },
 3292+
 3293+ 'checked': function(nodes, value, root) {
 3294+ for (var i = 0, results = [], node; node = nodes[i]; i++)
 3295+ if (node.checked) results.push(node);
 3296+ return results;
 3297+ }
 3298+ },
 3299+
 3300+ operators: {
 3301+ '=': function(nv, v) { return nv == v; },
 3302+ '!=': function(nv, v) { return nv != v; },
 3303+ '^=': function(nv, v) { return nv.startsWith(v); },
 3304+ '$=': function(nv, v) { return nv.endsWith(v); },
 3305+ '*=': function(nv, v) { return nv.include(v); },
 3306+ '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
 3307+ '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
 3308+ },
 3309+
 3310+ matchElements: function(elements, expression) {
 3311+ var matches = new Selector(expression).findElements(), h = Selector.handlers;
 3312+ h.mark(matches);
 3313+ for (var i = 0, results = [], element; element = elements[i]; i++)
 3314+ //TS: does not work in IE: if (element._counted) results.push(element);
 3315+ if (element._counted == true) results.push(element);
 3316+ h.unmark(matches);
 3317+ return results;
 3318+ },
 3319+
 3320+ findElement: function(elements, expression, index) {
 3321+ if (Object.isNumber(expression)) {
 3322+ index = expression; expression = false;
 3323+ }
 3324+ return Selector.matchElements(elements, expression || '*')[index || 0];
 3325+ },
 3326+
 3327+ findChildElements: function(element, expressions) {
 3328+ var exprs = expressions.join(','), expressions = [];
 3329+ exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
 3330+ expressions.push(m[1].strip());
 3331+ });
 3332+ var results = [], h = Selector.handlers;
 3333+ for (var i = 0, l = expressions.length, selector; i < l; i++) {
 3334+ selector = new Selector(expressions[i].strip());
 3335+ h.concat(results, selector.findElements(element));
 3336+ }
 3337+ return (l > 1) ? h.unique(results) : results;
 3338+ }
 3339+});
 3340+
 3341+function $$() {
 3342+ return Selector.findChildElements(document, $A(arguments));
 3343+}
 3344+var Form = {
 3345+ reset: function(form) {
 3346+ $(form).reset();
 3347+ return form;
 3348+ },
 3349+
 3350+ serializeElements: function(elements, options) {
 3351+ if (typeof options != 'object') options = { hash: !!options };
 3352+ else if (options.hash === undefined) options.hash = true;
 3353+ var key, value, submitted = false, submit = options.submit;
 3354+
 3355+ var data = elements.inject({ }, function(result, element) {
 3356+ if (!element.disabled && element.name) {
 3357+ key = element.name; value = $(element).getValue();
 3358+ if (value != null && (element.type != 'submit' || (!submitted &&
 3359+ submit !== false && (!submit || key == submit) && (submitted = true)))) {
 3360+ if (key in result) {
 3361+ // a key is already present; construct an array of values
 3362+ if (!Object.isArray(result[key])) result[key] = [result[key]];
 3363+ result[key].push(value);
 3364+ }
 3365+ else result[key] = value;
 3366+ }
 3367+ }
 3368+ return result;
 3369+ });
 3370+
 3371+ return options.hash ? data : Object.toQueryString(data);
 3372+ }
 3373+};
 3374+
 3375+Form.Methods = {
 3376+ serialize: function(form, options) {
 3377+ return Form.serializeElements(Form.getElements(form), options);
 3378+ },
 3379+
 3380+ getElements: function(form) {
 3381+ return $A($(form).getElementsByTagName('*')).inject([],
 3382+ function(elements, child) {
 3383+ if (Form.Element.Serializers[child.tagName.toLowerCase()])
 3384+ elements.push(Element.extend(child));
 3385+ return elements;
 3386+ }
 3387+ );
 3388+ },
 3389+
 3390+ getInputs: function(form, typeName, name) {
 3391+ form = $(form);
 3392+ var inputs = form.getElementsByTagName('input');
 3393+
 3394+ if (!typeName && !name) return $A(inputs).map(Element.extend);
 3395+
 3396+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
 3397+ var input = inputs[i];
 3398+ if ((typeName && input.type != typeName) || (name && input.name != name))
 3399+ continue;
 3400+ matchingInputs.push(Element.extend(input));
 3401+ }
 3402+
 3403+ return matchingInputs;
 3404+ },
 3405+
 3406+ disable: function(form) {
 3407+ form = $(form);
 3408+ Form.getElements(form).invoke('disable');
 3409+ return form;
 3410+ },
 3411+
 3412+ enable: function(form) {
 3413+ form = $(form);
 3414+ Form.getElements(form).invoke('enable');
 3415+ return form;
 3416+ },
 3417+
 3418+ findFirstElement: function(form) {
 3419+ var elements = $(form).getElements().findAll(function(element) {
 3420+ return 'hidden' != element.type && !element.disabled;
 3421+ });
 3422+ var firstByIndex = elements.findAll(function(element) {
 3423+ return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
 3424+ }).sortBy(function(element) { return element.tabIndex }).first();
 3425+
 3426+ return firstByIndex ? firstByIndex : elements.find(function(element) {
 3427+ return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
 3428+ });
 3429+ },
 3430+
 3431+ focusFirstElement: function(form) {
 3432+ form = $(form);
 3433+ form.findFirstElement().activate();
 3434+ return form;
 3435+ },
 3436+
 3437+ request: function(form, options) {
 3438+ form = $(form), options = Object.clone(options || { });
 3439+
 3440+ var params = options.parameters, action = form.readAttribute('action') || '';
 3441+ if (action.blank()) action = window.location.href;
 3442+ options.parameters = form.serialize(true);
 3443+
 3444+ if (params) {
 3445+ if (Object.isString(params)) params = params.toQueryParams();
 3446+ Object.extend(options.parameters, params);
 3447+ }
 3448+
 3449+ if (form.hasAttribute('method') && !options.method)
 3450+ options.method = form.method;
 3451+
 3452+ return new Ajax.Request(action, options);
 3453+ }
 3454+};
 3455+
 3456+/*--------------------------------------------------------------------------*/
 3457+
 3458+Form.Element = {
 3459+ focus: function(element) {
 3460+ $(element).focus();
 3461+ return element;
 3462+ },
 3463+
 3464+ select: function(element) {
 3465+ $(element).select();
 3466+ return element;
 3467+ }
 3468+};
 3469+
 3470+Form.Element.Methods = {
 3471+ serialize: function(element) {
 3472+ element = $(element);
 3473+ if (!element.disabled && element.name) {
 3474+ var value = element.getValue();
 3475+ if (value != undefined) {
 3476+ var pair = { };
 3477+ pair[element.name] = value;
 3478+ return Object.toQueryString(pair);
 3479+ }
 3480+ }
 3481+ return '';
 3482+ },
 3483+
 3484+ getValue: function(element) {
 3485+ element = $(element);
 3486+ var method = element.tagName.toLowerCase();
 3487+ return Form.Element.Serializers[method](element);
 3488+ },
 3489+
 3490+ setValue: function(element, value) {
 3491+ element = $(element);
 3492+ var method = element.tagName.toLowerCase();
 3493+ Form.Element.Serializers[method](element, value);
 3494+ return element;
 3495+ },
 3496+
 3497+ clear: function(element) {
 3498+ $(element).value = '';
 3499+ return element;
 3500+ },
 3501+
 3502+ present: function(element) {
 3503+ return $(element).value != '';
 3504+ },
 3505+
 3506+ activate: function(element) {
 3507+ element = $(element);
 3508+ try {
 3509+ element.focus();
 3510+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
 3511+ !['button', 'reset', 'submit'].include(element.type)))
 3512+ element.select();
 3513+ } catch (e) { }
 3514+ return element;
 3515+ },
 3516+
 3517+ disable: function(element) {
 3518+ element = $(element);
 3519+ element.blur();
 3520+ element.disabled = true;
 3521+ return element;
 3522+ },
 3523+
 3524+ enable: function(element) {
 3525+ element = $(element);
 3526+ element.disabled = false;
 3527+ return element;
 3528+ }
 3529+};
 3530+
 3531+/*--------------------------------------------------------------------------*/
 3532+
 3533+var Field = Form.Element;
 3534+var $F = Form.Element.Methods.getValue;
 3535+
 3536+/*--------------------------------------------------------------------------*/
 3537+
 3538+Form.Element.Serializers = {
 3539+ input: function(element, value) {
 3540+ switch (element.type.toLowerCase()) {
 3541+ case 'checkbox':
 3542+ case 'radio':
 3543+ return Form.Element.Serializers.inputSelector(element, value);
 3544+ default:
 3545+ return Form.Element.Serializers.textarea(element, value);
 3546+ }
 3547+ },
 3548+
 3549+ inputSelector: function(element, value) {
 3550+ if (value === undefined) return element.checked ? element.value : null;
 3551+ else element.checked = !!value;
 3552+ },
 3553+
 3554+ textarea: function(element, value) {
 3555+ if (value === undefined) return element.value;
 3556+ else element.value = value;
 3557+ },
 3558+
 3559+ select: function(element, index) {
 3560+ if (index === undefined)
 3561+ return this[element.type == 'select-one' ?
 3562+ 'selectOne' : 'selectMany'](element);
 3563+ else {
 3564+ var opt, value, single = !Object.isArray(index);
 3565+ for (var i = 0, length = element.length; i < length; i++) {
 3566+ opt = element.options[i];
 3567+ value = this.optionValue(opt);
 3568+ if (single) {
 3569+ if (value == index) {
 3570+ opt.selected = true;
 3571+ return;
 3572+ }
 3573+ }
 3574+ else opt.selected = index.include(value);
 3575+ }
 3576+ }
 3577+ },
 3578+
 3579+ selectOne: function(element) {
 3580+ var index = element.selectedIndex;
 3581+ return index >= 0 ? this.optionValue(element.options[index]) : null;
 3582+ },
 3583+
 3584+ selectMany: function(element) {
 3585+ var values, length = element.length;
 3586+ if (!length) return null;
 3587+
 3588+ for (var i = 0, values = []; i < length; i++) {
 3589+ var opt = element.options[i];
 3590+ if (opt.selected) values.push(this.optionValue(opt));
 3591+ }
 3592+ return values;
 3593+ },
 3594+
 3595+ optionValue: function(opt) {
 3596+ // extend element because hasAttribute may not be native
 3597+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
 3598+ }
 3599+};
 3600+
 3601+/*--------------------------------------------------------------------------*/
 3602+
 3603+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
 3604+ initialize: function($super, element, frequency, callback) {
 3605+ $super(callback, frequency);
 3606+ this.element = $(element);
 3607+ this.lastValue = this.getValue();
 3608+ },
 3609+
 3610+ execute: function() {
 3611+ var value = this.getValue();
 3612+ if (Object.isString(this.lastValue) && Object.isString(value) ?
 3613+ this.lastValue != value : String(this.lastValue) != String(value)) {
 3614+ this.callback(this.element, value);
 3615+ this.lastValue = value;
 3616+ }
 3617+ }
 3618+});
 3619+
 3620+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
 3621+ getValue: function() {
 3622+ return Form.Element.getValue(this.element);
 3623+ }
 3624+});
 3625+
 3626+Form.Observer = Class.create(Abstract.TimedObserver, {
 3627+ getValue: function() {
 3628+ return Form.serialize(this.element);
 3629+ }
 3630+});
 3631+
 3632+/*--------------------------------------------------------------------------*/
 3633+
 3634+Abstract.EventObserver = Class.create({
 3635+ initialize: function(element, callback) {
 3636+ this.element = $(element);
 3637+ this.callback = callback;
 3638+
 3639+ this.lastValue = this.getValue();
 3640+ if (this.element.tagName.toLowerCase() == 'form')
 3641+ this.registerFormCallbacks();
 3642+ else
 3643+ this.registerCallback(this.element);
 3644+ },
 3645+
 3646+ onElementEvent: function() {
 3647+ var value = this.getValue();
 3648+ if (this.lastValue != value) {
 3649+ this.callback(this.element, value);
 3650+ this.lastValue = value;
 3651+ }
 3652+ },
 3653+
 3654+ registerFormCallbacks: function() {
 3655+ Form.getElements(this.element).each(this.registerCallback, this);
 3656+ },
 3657+
 3658+ registerCallback: function(element) {
 3659+ if (element.type) {
 3660+ switch (element.type.toLowerCase()) {
 3661+ case 'checkbox':
 3662+ case 'radio':
 3663+ Event.observe(element, 'click', this.onElementEvent.bind(this));
 3664+ break;
 3665+ default:
 3666+ Event.observe(element, 'change', this.onElementEvent.bind(this));
 3667+ break;
 3668+ }
 3669+ }
 3670+ }
 3671+});
 3672+
 3673+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
 3674+ getValue: function() {
 3675+ return Form.Element.getValue(this.element);
 3676+ }
 3677+});
 3678+
 3679+Form.EventObserver = Class.create(Abstract.EventObserver, {
 3680+ getValue: function() {
 3681+ return Form.serialize(this.element);
 3682+ }
 3683+});
 3684+if (!window.Event) var Event = { };
 3685+
 3686+Object.extend(Event, {
 3687+ KEY_BACKSPACE: 8,
 3688+ KEY_TAB: 9,
 3689+ KEY_RETURN: 13,
 3690+ KEY_ESC: 27,
 3691+ KEY_LEFT: 37,
 3692+ KEY_UP: 38,
 3693+ KEY_RIGHT: 39,
 3694+ KEY_DOWN: 40,
 3695+ KEY_DELETE: 46,
 3696+ KEY_HOME: 36,
 3697+ KEY_END: 35,
 3698+ KEY_PAGEUP: 33,
 3699+ KEY_PAGEDOWN: 34,
 3700+ KEY_INSERT: 45,
 3701+
 3702+ cache: { },
 3703+
 3704+ relatedTarget: function(event) {
 3705+ var element;
 3706+ switch(event.type) {
 3707+ case 'mouseover': element = event.fromElement; break;
 3708+ case 'mouseout': element = event.toElement; break;
 3709+ default: return null;
 3710+ }
 3711+ return Element.extend(element);
 3712+ }
 3713+});
 3714+
 3715+Event.Methods = (function() {
 3716+ var isButton;
 3717+
 3718+ if (Prototype.Browser.IE) {
 3719+ var buttonMap = { 0: 1, 1: 4, 2: 2 };
 3720+ isButton = function(event, code) {
 3721+ return event.button == buttonMap[code];
 3722+ };
 3723+
 3724+ } else if (Prototype.Browser.WebKit) {
 3725+ isButton = function(event, code) {
 3726+ switch (code) {
 3727+ case 0: return event.which == 1 && !event.metaKey;
 3728+ case 1: return event.which == 1 && event.metaKey;
 3729+ default: return false;
 3730+ }
 3731+ };
 3732+
 3733+ } else {
 3734+ isButton = function(event, code) {
 3735+ return event.which ? (event.which === code + 1) : (event.button === code);
 3736+ };
 3737+ }
 3738+
 3739+ return {
 3740+ isLeftClick: function(event) { return isButton(event, 0) },
 3741+ isMiddleClick: function(event) { return isButton(event, 1) },
 3742+ isRightClick: function(event) { return isButton(event, 2) },
 3743+
 3744+ element: function(event) {
 3745+ var node = Event.extend(event).target;
 3746+ return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
 3747+ },
 3748+
 3749+ findElement: function(event, expression) {
 3750+ var element = Event.element(event);
 3751+ return element.match(expression) ? element : element.up(expression);
 3752+ },
 3753+
 3754+ pointer: function(event) {
 3755+ return {
 3756+ x: event.pageX || (event.clientX +
 3757+ (document.documentElement.scrollLeft || document.body.scrollLeft)),
 3758+ y: event.pageY || (event.clientY +
 3759+ (document.documentElement.scrollTop || document.body.scrollTop))
 3760+ };
 3761+ },
 3762+
 3763+ pointerX: function(event) { return Event.pointer(event).x },
 3764+ pointerY: function(event) { return Event.pointer(event).y },
 3765+
 3766+ stop: function(event) {
 3767+ Event.extend(event);
 3768+ event.preventDefault();
 3769+ event.stopPropagation();
 3770+ event.stopped = true;
 3771+ }
 3772+ };
 3773+})();
 3774+
 3775+Event.extend = (function() {
 3776+ var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
 3777+ m[name] = Event.Methods[name].methodize();
 3778+ return m;
 3779+ });
 3780+
 3781+ if (Prototype.Browser.IE) {
 3782+ Object.extend(methods, {
 3783+ stopPropagation: function() { this.cancelBubble = true },
 3784+ preventDefault: function() { this.returnValue = false },
 3785+ inspect: function() { return "[object Event]" }
 3786+ });
 3787+
 3788+ return function(event) {
 3789+ if (!event) return false;
 3790+ if (event._extendedByPrototype) return event;
 3791+
 3792+ event._extendedByPrototype = Prototype.emptyFunction;
 3793+ var pointer = Event.pointer(event);
 3794+ Object.extend(event, {
 3795+ target: event.srcElement,
 3796+ relatedTarget: Event.relatedTarget(event),
 3797+ pageX: pointer.x,
 3798+ pageY: pointer.y
 3799+ });
 3800+ return Object.extend(event, methods);
 3801+ };
 3802+
 3803+ } else {
 3804+ Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;
 3805+ Object.extend(Event.prototype, methods);
 3806+ return Prototype.K;
 3807+ }
 3808+})();
 3809+
 3810+Object.extend(Event, (function() {
 3811+ var cache = Event.cache;
 3812+
 3813+ function getEventID(element) {
 3814+ if (element._eventID) return element._eventID;
 3815+ arguments.callee.id = arguments.callee.id || 1;
 3816+ return element._eventID = ++arguments.callee.id;
 3817+ }
 3818+
 3819+ function getDOMEventName(eventName) {
 3820+ if (eventName && eventName.include(':')) return "dataavailable";
 3821+ return eventName;
 3822+ }
 3823+
 3824+ function getCacheForID(id) {
 3825+ return cache[id] = cache[id] || { };
 3826+ }
 3827+
 3828+ function getWrappersForEventName(id, eventName) {
 3829+ var c = getCacheForID(id);
 3830+ return c[eventName] = c[eventName] || [];
 3831+ }
 3832+
 3833+ function createWrapper(element, eventName, handler) {
 3834+ var id = getEventID(element);
 3835+ var c = getWrappersForEventName(id, eventName);
 3836+ if (c.pluck("handler").include(handler)) return false;
 3837+
 3838+ var wrapper = function(event) {
 3839+ if (!Event || !Event.extend ||
 3840+ (event.eventName && event.eventName != eventName))
 3841+ return false;
 3842+
 3843+ Event.extend(event);
 3844+ handler.call(element, event)
 3845+ };
 3846+
 3847+ wrapper.handler = handler;
 3848+ c.push(wrapper);
 3849+ return wrapper;
 3850+ }
 3851+
 3852+ function findWrapper(id, eventName, handler) {
 3853+ var c = getWrappersForEventName(id, eventName);
 3854+ return c.find(function(wrapper) { return wrapper.handler == handler });
 3855+ }
 3856+
 3857+ function destroyWrapper(id, eventName, handler) {
 3858+ var c = getCacheForID(id);
 3859+ if (!c[eventName]) return false;
 3860+ c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
 3861+ }
 3862+
 3863+ function destroyCache() {
 3864+ for (var id in cache)
 3865+ for (var eventName in cache[id])
 3866+ cache[id][eventName] = null;
 3867+ }
 3868+
 3869+ if (window.attachEvent) {
 3870+ window.attachEvent("onunload", destroyCache);
 3871+ }
 3872+
 3873+ return {
 3874+ observe: function(element, eventName, handler) {
 3875+ element = $(element);
 3876+ var name = getDOMEventName(eventName);
 3877+
 3878+ var wrapper = createWrapper(element, eventName, handler);
 3879+ if (!wrapper) return element;
 3880+
 3881+ if (element.addEventListener) {
 3882+ element.addEventListener(name, wrapper, false);
 3883+ } else {
 3884+ element.attachEvent("on" + name, wrapper);
 3885+ }
 3886+
 3887+ return element;
 3888+ },
 3889+
 3890+ stopObserving: function(element, eventName, handler) {
 3891+ element = $(element);
 3892+ var id = getEventID(element), name = getDOMEventName(eventName);
 3893+
 3894+ if (!handler && eventName) {
 3895+ getWrappersForEventName(id, eventName).each(function(wrapper) {
 3896+ element.stopObserving(eventName, wrapper.handler);
 3897+ });
 3898+ return element;
 3899+
 3900+ } else if (!eventName) {
 3901+ Object.keys(getCacheForID(id)).each(function(eventName) {
 3902+ element.stopObserving(eventName);
 3903+ });
 3904+ return element;
 3905+ }
 3906+
 3907+ var wrapper = findWrapper(id, eventName, handler);
 3908+ if (!wrapper) return element;
 3909+
 3910+ if (element.removeEventListener) {
 3911+ element.removeEventListener(name, wrapper, false);
 3912+ } else {
 3913+ element.detachEvent("on" + name, wrapper);
 3914+ }
 3915+
 3916+ destroyWrapper(id, eventName, handler);
 3917+
 3918+ return element;
 3919+ },
 3920+
 3921+ fire: function(element, eventName, memo) {
 3922+ element = $(element);
 3923+ if (element == document && document.createEvent && !element.dispatchEvent)
 3924+ element = document.documentElement;
 3925+
 3926+ if (document.createEvent) {
 3927+ var event = document.createEvent("HTMLEvents");
 3928+ event.initEvent("dataavailable", true, true);
 3929+ } else {
 3930+ var event = document.createEventObject();
 3931+ event.eventType = "ondataavailable";
 3932+ }
 3933+
 3934+ event.eventName = eventName;
 3935+ event.memo = memo || { };
 3936+
 3937+ if (document.createEvent) {
 3938+ element.dispatchEvent(event);
 3939+ } else {
 3940+ element.fireEvent(event.eventType, event);
 3941+ }
 3942+
 3943+ return event;
 3944+ }
 3945+ };
 3946+})());
 3947+
 3948+Object.extend(Event, Event.Methods);
 3949+
 3950+Element.addMethods({
 3951+ fire: Event.fire,
 3952+ observe: Event.observe,
 3953+ stopObserving: Event.stopObserving
 3954+});
 3955+
 3956+Object.extend(document, {
 3957+ fire: Element.Methods.fire.methodize(),
 3958+ observe: Element.Methods.observe.methodize(),
 3959+ stopObserving: Element.Methods.stopObserving.methodize()
 3960+});
 3961+
 3962+(function() {
 3963+ /* Support for the DOMContentLoaded event is based on work by Dan Webb,
 3964+ Matthias Miller, Dean Edwards and John Resig. */
 3965+
 3966+ var timer, fired = false;
 3967+
 3968+ function fireContentLoadedEvent() {
 3969+ if (fired) return;
 3970+ if (timer) window.clearInterval(timer);
 3971+ document.fire("dom:loaded");
 3972+ fired = true;
 3973+ }
 3974+
 3975+ if (document.addEventListener) {
 3976+ if (Prototype.Browser.WebKit) {
 3977+ timer = window.setInterval(function() {
 3978+ if (/loaded|complete/.test(document.readyState))
 3979+ fireContentLoadedEvent();
 3980+ }, 0);
 3981+
 3982+ Event.observe(window, "load", fireContentLoadedEvent);
 3983+
 3984+ } else {
 3985+ document.addEventListener("DOMContentLoaded",
 3986+ fireContentLoadedEvent, false);
 3987+ }
 3988+
 3989+ } else {
 3990+ document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
 3991+ $("__onDOMContentLoaded").onreadystatechange = function() {
 3992+ if (this.readyState == "complete") {
 3993+ this.onreadystatechange = null;
 3994+ fireContentLoadedEvent();
 3995+ }
 3996+ };
 3997+ }
 3998+})();
 3999+/*------------------------------- DEPRECATED -------------------------------*/
 4000+
 4001+Hash.toQueryString = Object.toQueryString;
 4002+
 4003+var Toggle = { display: Element.toggle };
 4004+
 4005+Element.Methods.childOf = Element.Methods.descendantOf;
 4006+
 4007+var Insertion = {
 4008+ Before: function(element, content) {
 4009+ return Element.insert(element, {before:content});
 4010+ },
 4011+
 4012+ Top: function(element, content) {
 4013+ return Element.insert(element, {top:content});
 4014+ },
 4015+
 4016+ Bottom: function(element, content) {
 4017+ return Element.insert(element, {bottom:content});
 4018+ },
 4019+
 4020+ After: function(element, content) {
 4021+ return Element.insert(element, {after:content});
 4022+ }
 4023+};
 4024+
 4025+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
 4026+
 4027+// This should be moved to script.aculo.us; notice the deprecated methods
 4028+// further below, that map to the newer Element methods.
 4029+var Position = {
 4030+ // set to true if needed, warning: firefox performance problems
 4031+ // NOT neeeded for page scrolling, only if draggable contained in
 4032+ // scrollable elements
 4033+ includeScrollOffsets: false,
 4034+
 4035+ // must be called before calling withinIncludingScrolloffset, every time the
 4036+ // page is scrolled
 4037+ prepare: function() {
 4038+ this.deltaX = window.pageXOffset
 4039+ || document.documentElement.scrollLeft
 4040+ || document.body.scrollLeft
 4041+ || 0;
 4042+ this.deltaY = window.pageYOffset
 4043+ || document.documentElement.scrollTop
 4044+ || document.body.scrollTop
 4045+ || 0;
 4046+ },
 4047+
 4048+ // caches x/y coordinate pair to use with overlap
 4049+ within: function(element, x, y) {
 4050+ if (this.includeScrollOffsets)
 4051+ return this.withinIncludingScrolloffsets(element, x, y);
 4052+ this.xcomp = x;
 4053+ this.ycomp = y;
 4054+ this.offset = Element.cumulativeOffset(element);
 4055+
 4056+ return (y >= this.offset[1] &&
 4057+ y < this.offset[1] + element.offsetHeight &&
 4058+ x >= this.offset[0] &&
 4059+ x < this.offset[0] + element.offsetWidth);
 4060+ },
 4061+
 4062+ withinIncludingScrolloffsets: function(element, x, y) {
 4063+ var offsetcache = Element.cumulativeScrollOffset(element);
 4064+
 4065+ this.xcomp = x + offsetcache[0] - this.deltaX;
 4066+ this.ycomp = y + offsetcache[1] - this.deltaY;
 4067+ this.offset = Element.cumulativeOffset(element);
 4068+
 4069+ return (this.ycomp >= this.offset[1] &&
 4070+ this.ycomp < this.offset[1] + element.offsetHeight &&
 4071+ this.xcomp >= this.offset[0] &&
 4072+ this.xcomp < this.offset[0] + element.offsetWidth);
 4073+ },
 4074+
 4075+ // within must be called directly before
 4076+ overlap: function(mode, element) {
 4077+ if (!mode) return 0;
 4078+ if (mode == 'vertical')
 4079+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
 4080+ element.offsetHeight;
 4081+ if (mode == 'horizontal')
 4082+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
 4083+ element.offsetWidth;
 4084+ },
 4085+
 4086+ // Deprecation layer -- use newer Element methods now (1.5.2).
 4087+
 4088+ cumulativeOffset: Element.Methods.cumulativeOffset,
 4089+
 4090+ positionedOffset: Element.Methods.positionedOffset,
 4091+
 4092+ absolutize: function(element) {
 4093+ Position.prepare();
 4094+ return Element.absolutize(element);
 4095+ },
 4096+
 4097+ relativize: function(element) {
 4098+ Position.prepare();
 4099+ return Element.relativize(element);
 4100+ },
 4101+
 4102+ realOffset: Element.Methods.cumulativeScrollOffset,
 4103+
 4104+ offsetParent: Element.Methods.getOffsetParent,
 4105+
 4106+ page: Element.Methods.viewportOffset,
 4107+
 4108+ clone: function(source, target, options) {
 4109+ options = options || { };
 4110+ return Element.clonePosition(target, source, options);
 4111+ }
 4112+};
 4113+
 4114+/*--------------------------------------------------------------------------*/
 4115+
 4116+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
 4117+ function iter(name) {
 4118+ return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
 4119+ }
 4120+
 4121+ instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
 4122+ function(element, className) {
 4123+ className = className.toString().strip();
 4124+ var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
 4125+ return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
 4126+ } : function(element, className) {
 4127+ className = className.toString().strip();
 4128+ var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
 4129+ if (!classNames && !className) return elements;
 4130+
 4131+ var nodes = $(element).getElementsByTagName('*');
 4132+ className = ' ' + className + ' ';
 4133+
 4134+ for (var i = 0, child, cn; child = nodes[i]; i++) {
 4135+ if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
 4136+ (classNames && classNames.all(function(name) {
 4137+ return !name.toString().blank() && cn.include(' ' + name + ' ');
 4138+ }))))
 4139+ elements.push(Element.extend(child));
 4140+ }
 4141+ return elements;
 4142+ };
 4143+
 4144+ return function(className, parentElement) {
 4145+ return $(parentElement || document.body).getElementsByClassName(className);
 4146+ };
 4147+}(Element.Methods);
 4148+
 4149+/*--------------------------------------------------------------------------*/
 4150+
 4151+Element.ClassNames = Class.create();
 4152+Element.ClassNames.prototype = {
 4153+ initialize: function(element) {
 4154+ this.element = $(element);
 4155+ },
 4156+
 4157+ _each: function(iterator) {
 4158+ this.element.className.split(/\s+/).select(function(name) {
 4159+ return name.length > 0;
 4160+ })._each(iterator);
 4161+ },
 4162+
 4163+ set: function(className) {
 4164+ this.element.className = className;
 4165+ },
 4166+
 4167+ add: function(classNameToAdd) {
 4168+ if (this.include(classNameToAdd)) return;
 4169+ this.set($A(this).concat(classNameToAdd).join(' '));
 4170+ },
 4171+
 4172+ remove: function(classNameToRemove) {
 4173+ if (!this.include(classNameToRemove)) return;
 4174+ this.set($A(this).without(classNameToRemove).join(' '));
 4175+ },
 4176+
 4177+ toString: function() {
 4178+ return $A(this).join(' ');
 4179+ }
 4180+};
 4181+
 4182+Object.extend(Element.ClassNames.prototype, Enumerable);
 4183+
 4184+/*--------------------------------------------------------------------------*/
 4185+
 4186+Element.addMethods();
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/scripts/Language/SMW_NMLanguage.js
@@ -0,0 +1,75 @@
 2+/**
 3+* SMW_NMLanguage.js
 4+*
 5+* A class that reads language strings from the server by an ajax call.
 6+*
 7+* @author dch
 8+*
 9+*/
 10+
 11+var NMLanguage = Class.create();
 12+
 13+/**
 14+ * This class provides language dependent strings for an identifier.
 15+ *
 16+ */
 17+NMLanguage.prototype = {
 18+
 19+ /**
 20+ * @public
 21+ *
 22+ * Constructor.
 23+ */
 24+ initialize: function() {
 25+ },
 26+
 27+ /*
 28+ * @public
 29+ *
 30+ * Returns a language dependent message for an ID, or the ID, if there is
 31+ * no message for it.
 32+ *
 33+ * @param string id
 34+ * ID of the message to be retrieved.
 35+ * @return string
 36+ * The language dependent message for the given ID.
 37+ */
 38+ getMessage: function(id, type) {
 39+ switch (type) {
 40+ case "user":
 41+ var msg = wgNMUserLanguageStrings[id];
 42+ if (!msg) {
 43+ msg = id;
 44+ }
 45+ break;
 46+
 47+ case "cont":
 48+ var msg = wgNMContLanguageStrings[id];
 49+ if (!msg) {
 50+ msg = id;
 51+ }
 52+ break;
 53+ default:
 54+ var msg = wgNMUserLanguageStrings[id];
 55+ if (!msg) {
 56+ var msg = wgNMContLanguageStrings[id];
 57+ if (!msg) {
 58+ msg = id;
 59+ }
 60+ }
 61+ }
 62+
 63+ // Replace variables
 64+ msg = msg.replace(/\$n/g,wgCanonicalNamespace);
 65+ msg = msg.replace(/\$p/g,wgPageName);
 66+ msg = msg.replace(/\$t/g,wgTitle);
 67+ msg = msg.replace(/\$u/g,wgUserName);
 68+ msg = msg.replace(/\$s/g,wgServer);
 69+ return msg;
 70+ }
 71+
 72+}
 73+
 74+// Singleton of this class
 75+
 76+var nmLanguage = new NMLanguage();
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/scripts/Language/SMW_NMLanguageUserEn.js
@@ -0,0 +1,4 @@
 2+var wgNMUserLanguageStrings = {
 3+ 'NM_EMPTY_NOTIFYNAME' : 'Your Notify-name is empty.',
 4+ 'NM_EMPTY_QUERY' : 'Your query is empty.',
 5+};
Index: trunk/extensions/SemanticNotifyMe/scripts/Language/SMW_NMLanguageEn.js
@@ -0,0 +1,3 @@
 2+var wgNMContLanguageStrings = {
 3+ 'NOTIFYME_NS' : 'NotifyMe:' //namespace identifier with colon
 4+}
\ No newline at end of file
Index: trunk/extensions/SemanticNotifyMe/scripts/NotifyMe/nm_tooltip.js
@@ -0,0 +1,1149 @@
 2+/* This notice must be untouched at all times.
 3+
 4+wz_tooltip.js v. 4.12
 5+
 6+The latest version is available at
 7+http://www.walterzorn.com
 8+or http://www.devira.com
 9+or http://www.walterzorn.de
 10+
 11+Copyright (c) 2002-2007 Walter Zorn. All rights reserved.
 12+Created 1.12.2002 by Walter Zorn (Web: http://www.walterzorn.com )
 13+Last modified: 13.7.2007
 14+
 15+Easy-to-use cross-browser tooltips.
 16+Just include the script at the beginning of the <body> section, and invoke
 17+Tip('Tooltip text') from within the desired HTML onmouseover eventhandlers.
 18+No container DIV, no onmouseouts required.
 19+By default, width of tooltips is automatically adapted to content.
 20+Is even capable of dynamically converting arbitrary HTML elements to tooltips
 21+by calling TagToTip('ID_of_HTML_element_to_be_converted') instead of Tip(),
 22+which means you can put important, search-engine-relevant stuff into tooltips.
 23+Appearance of tooltips can be individually configured
 24+via commands passed to Tip() or TagToTip().
 25+
 26+Tab Width: 4
 27+LICENSE: LGPL
 28+
 29+This library is free software; you can redistribute it and/or
 30+modify it under the terms of the GNU Lesser General Public
 31+License (LGPL) as published by the Free Software Foundation; either
 32+version 2.1 of the License, or (at your option) any later version.
 33+
 34+This library is distributed in the hope that it will be useful,
 35+but WITHOUT ANY WARRANTY; without even the implied warranty of
 36+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 37+
 38+For more details on the GNU Lesser General Public License,
 39+see http://www.gnu.org/copyleft/lesser.html
 40+*/
 41+
 42+var config = new Object();
 43+
 44+
 45+//=================== GLOBAL TOOPTIP CONFIGURATION =========================//
 46+var tt_Debug = false // false or true - recommended: false once you release your page to the public
 47+var tt_Enabled = true // Allows to (temporarily) suppress tooltips, e.g. by providing the user with a button that sets this global variable to false
 48+var TagsToTip = true // false or true - if true, the script is capable of converting HTML elements to tooltips
 49+
 50+// For each of the following config variables there exists a command, which is
 51+// just the variablename in uppercase, to be passed to Tip() or TagToTip() to
 52+// configure tooltips individually. Individual commands override global
 53+// configuration. Order of commands is arbitrary.
 54+// Example: onmouseover="Tip('Tooltip text', LEFT, true, BGCOLOR, '#FF9900', FADEIN, 400)"
 55+
 56+config. Above = false // false or true - tooltip above mousepointer?
 57+config. BgColor = '#E4E7FF' // Background color
 58+config. BgImg = '' // Path to background image, none if empty string ''
 59+config. BorderColor = '#002299'
 60+config. BorderStyle = 'solid' // Any permitted CSS value, but I recommend 'solid', 'dotted' or 'dashed'
 61+config. BorderWidth = 1
 62+config. CenterMouse = false // false or true - center the tip horizontally below (or above) the mousepointer
 63+config. ClickClose = false // false or true - close tooltip if the user clicks somewhere
 64+config. CloseBtn = false // false or true - closebutton in titlebar
 65+config. CloseBtnColors = ['#990000', '#FFFFFF', '#DD3333', '#FFFFFF'] // [Background, text, hovered background, hovered text] - use empty strings '' to inherit title colors
 66+config. CloseBtnText = '&nbsp;X&nbsp;' // Close button text (may also be an image tag)
 67+config. CopyContent = true // When converting a HTML element to a tooltip, copy only the element's content, rather than converting the element by its own
 68+config. Delay = 400 // Time span in ms until tooltip shows up
 69+config. Duration = 0 // Time span in ms after which the tooltip disappears; 0 for infinite duration
 70+config. FadeIn = 0 // Fade-in duration in ms, e.g. 400; 0 for no animation
 71+config. FadeOut = 0
 72+config. FadeInterval = 30 // Duration of each fade step in ms (recommended: 30) - shorter is smoother but causes more CPU-load
 73+config. Fix = null // Fixated position - x- an y-oordinates in brackets, e.g. [210, 480], or null for no fixation
 74+config. FollowMouse = true // false or true - tooltip follows the mouse
 75+config. FontColor = '#000044'
 76+config. FontFace = 'Verdana,Geneva,sans-serif'
 77+config. FontSize = '8pt' // E.g. '9pt' or '12px' - unit is mandatory
 78+config. FontWeight = 'normal' // 'normal' or 'bold';
 79+config. Left = false // false or true - tooltip on the left of the mouse
 80+config. OffsetX = 14 // Horizontal offset of left-top corner from mousepointer
 81+config. OffsetY = 8 // Vertical offset
 82+config. Opacity = 100 // Integer between 0 and 100 - opacity of tooltip in percent
 83+config. Padding = 3 // Spacing between border and content
 84+config. Shadow = false // false or true
 85+config. ShadowColor = '#C0C0C0'
 86+config. ShadowWidth = 5
 87+config. Sticky = false // Do NOT hide tooltip on mouseout? false or true
 88+config. TextAlign = 'left' // 'left', 'right' or 'justify'
 89+config. Title = '' // Default title text applied to all tips (no default title: empty string '')
 90+config. TitleAlign = 'left' // 'left' or 'right' - text alignment inside the title bar
 91+config. TitleBgColor = '' // If empty string '', BorderColor will be used
 92+config. TitleFontColor = '#ffffff' // Color of title text - if '', BgColor (of tooltip body) will be used
 93+config. TitleFontFace = '' // If '' use FontFace (boldified)
 94+config. TitleFontSize = '' // If '' use FontSize
 95+config. Width = 200 // Tooltip width; 0 for automatic adaption to tooltip content
 96+//======= END OF TOOLTIP CONFIG, DO NOT CHANGE ANYTHING BELOW ==============//
 97+
 98+
 99+
 100+
 101+//====================== PUBLIC ============================================//
 102+function Tip()
 103+{
 104+ tt_Tip(arguments, null);
 105+}
 106+function TagToTip()
 107+{
 108+ if(TagsToTip)
 109+ {
 110+ var t2t = tt_GetElt(arguments[0]);
 111+ if(t2t)
 112+ tt_Tip(arguments, t2t);
 113+ }
 114+}
 115+
 116+//================== PUBLIC EXTENSION API ==================================//
 117+// Extension eventhandlers currently supported:
 118+// OnLoadConfig, OnCreateContentString, OnSubDivsCreated, OnShow, OnMoveBefore,
 119+// OnMoveAfter, OnHideInit, OnHide, OnKill
 120+
 121+var tt_aElt = new Array(10), // Container DIV, outer title & body DIVs, inner title & body TDs, closebutton SPAN, shadow DIVs, and IFRAME to cover windowed elements in IE
 122+tt_aV = new Array(), // Caches and enumerates config data for currently active tooltip
 123+tt_sContent, // Inner tooltip text or HTML
 124+tt_scrlX = 0, tt_scrlY = 0,
 125+tt_musX, tt_musY,
 126+tt_over,
 127+tt_x, tt_y, tt_w, tt_h; // Position, width and height of currently displayed tooltip
 128+
 129+function tt_Extension()
 130+{
 131+ tt_ExtCmdEnum();
 132+ tt_aExt[tt_aExt.length] = this;
 133+ return this;
 134+}
 135+function tt_SetTipPos(x, y)
 136+{
 137+ var css = tt_aElt[0].style;
 138+
 139+ tt_x = x;
 140+ tt_y = y;
 141+ css.left = x + "px";
 142+ css.top = y + "px";
 143+ if(tt_ie56)
 144+ {
 145+ var ifrm = tt_aElt[tt_aElt.length - 1];
 146+ if(ifrm)
 147+ {
 148+ ifrm.style.left = css.left;
 149+ ifrm.style.top = css.top;
 150+ }
 151+ }
 152+}
 153+function tt_Hide()
 154+{
 155+ if(tt_db && tt_iState)
 156+ {
 157+ if(tt_iState & 0x2)
 158+ {
 159+ tt_aElt[0].style.visibility = "hidden";
 160+ tt_ExtCallFncs(0, "Hide");
 161+ }
 162+ tt_tShow.EndTimer();
 163+ tt_tHide.EndTimer();
 164+ tt_tDurt.EndTimer();
 165+ tt_tFade.EndTimer();
 166+ if(!tt_op && !tt_ie)
 167+ {
 168+ tt_tWaitMov.EndTimer();
 169+ tt_bWait = false;
 170+ }
 171+ if(tt_aV[CLICKCLOSE])
 172+ tt_RemEvtFnc(document, "mouseup", tt_HideInit);
 173+ tt_AddRemOutFnc(false);
 174+ tt_ExtCallFncs(0, "Kill");
 175+ // In case of a TagToTip tooltip, hide converted DOM node and
 176+ // re-insert it into document
 177+ if(tt_t2t && !tt_aV[COPYCONTENT])
 178+ {
 179+ tt_t2t.style.display = "none";
 180+ tt_MovDomNode(tt_t2t, tt_aElt[6], tt_t2tDad);
 181+ }
 182+ tt_iState = 0;
 183+ tt_over = null;
 184+ tt_ResetMainDiv();
 185+ if(tt_aElt[tt_aElt.length - 1])
 186+ tt_aElt[tt_aElt.length - 1].style.display = "none";
 187+ }
 188+}
 189+function tt_GetElt(id)
 190+{
 191+ return(document.getElementById ? document.getElementById(id)
 192+ : document.all ? document.all[id]
 193+ : null);
 194+}
 195+function tt_GetDivW(el)
 196+{
 197+ return(el ? (el.offsetWidth || el.style.pixelWidth || 0) : 0);
 198+}
 199+function tt_GetDivH(el)
 200+{
 201+ return(el ? (el.offsetHeight || el.style.pixelHeight || 0) : 0);
 202+}
 203+function tt_GetScrollX()
 204+{
 205+ return(window.pageXOffset || (tt_db ? (tt_db.scrollLeft || 0) : 0));
 206+}
 207+function tt_GetScrollY()
 208+{
 209+ return(window.pageYOffset || (tt_db ? (tt_db.scrollTop || 0) : 0));
 210+}
 211+function tt_GetClientW()
 212+{
 213+ return(document.body && (typeof(document.body.clientWidth) != tt_u) ? document.body.clientWidth
 214+ : (typeof(window.innerWidth) != tt_u) ? window.innerWidth
 215+ : tt_db ? (tt_db.clientWidth || 0)
 216+ : 0);
 217+}
 218+function tt_GetClientH()
 219+{
 220+ // Exactly this order seems to yield correct values in all major browsers
 221+ return(document.body && (typeof(document.body.clientHeight) != tt_u) ? document.body.clientHeight
 222+ : (typeof(window.innerHeight) != tt_u) ? window.innerHeight
 223+ : tt_db ? (tt_db.clientHeight || 0)
 224+ : 0);
 225+}
 226+function tt_GetEvtX(e)
 227+{
 228+ return (e ? ((typeof(e.pageX) != tt_u) ? e.pageX : (e.clientX + tt_scrlX)) : 0);
 229+}
 230+function tt_GetEvtY(e)
 231+{
 232+ return (e ? ((typeof(e.pageY) != tt_u) ? e.pageY : (e.clientY + tt_scrlY)) : 0);
 233+}
 234+function tt_AddEvtFnc(el, sEvt, PFnc)
 235+{
 236+ if(el)
 237+ {
 238+ if(el.addEventListener)
 239+ el.addEventListener(sEvt, PFnc, false);
 240+ else
 241+ el.attachEvent("on" + sEvt, PFnc);
 242+ }
 243+}
 244+function tt_RemEvtFnc(el, sEvt, PFnc)
 245+{
 246+ if(el)
 247+ {
 248+ if(el.removeEventListener)
 249+ el.removeEventListener(sEvt, PFnc, false);
 250+ else
 251+ el.detachEvent("on" + sEvt, PFnc);
 252+ }
 253+}
 254+
 255+//====================== PRIVATE ===========================================//
 256+var tt_aExt = new Array(), // Array of extension objects
 257+
 258+tt_db, tt_op, tt_ie, tt_ie56, tt_bBoxOld, // Browser flags
 259+tt_body,
 260+tt_flagOpa, // Opacity support: 1=IE, 2=Khtml, 3=KHTML, 4=Moz, 5=W3C
 261+tt_maxPosX, tt_maxPosY,
 262+tt_iState = 0, // Tooltip active |= 1, shown |= 2, move with mouse |= 4
 263+tt_opa, // Currently applied opacity
 264+tt_bJmpVert, // Tip above mouse (or ABOVE tip below mouse)
 265+tt_t2t, tt_t2tDad, // Tag converted to tip, and its parent element in the document
 266+tt_elDeHref, // The tag from which Opera has removed the href attribute
 267+// Timer
 268+tt_tShow = new Number(0), tt_tHide = new Number(0), tt_tDurt = new Number(0),
 269+tt_tFade = new Number(0), tt_tWaitMov = new Number(0),
 270+tt_bWait = false,
 271+tt_u = "undefined";
 272+
 273+
 274+function tt_Init()
 275+{
 276+ tt_MkCmdEnum();
 277+ // Send old browsers instantly to hell
 278+ if(!tt_Browser() || !tt_MkMainDiv())
 279+ return;
 280+ tt_IsW3cBox();
 281+ tt_OpaSupport();
 282+ tt_AddEvtFnc(document, "mousemove", tt_Move);
 283+ // In Debug mode we search for TagToTip() calls in order to notify
 284+ // the user if they've forgotten to set the TagsToTip config flag
 285+ if(TagsToTip || tt_Debug)
 286+ tt_SetOnloadFnc();
 287+ tt_AddEvtFnc(window, "scroll",
 288+ function()
 289+ {
 290+ tt_scrlX = tt_GetScrollX();
 291+ tt_scrlY = tt_GetScrollY();
 292+ if(tt_iState && !(tt_aV[STICKY] && (tt_iState & 2)))
 293+ tt_HideInit();
 294+ } );
 295+ // Ensure the tip be hidden when the page unloads
 296+ tt_AddEvtFnc(window, "unload", tt_Hide);
 297+ tt_Hide();
 298+}
 299+// Creates command names by translating config variable names to upper case
 300+function tt_MkCmdEnum()
 301+{
 302+ var n = 0;
 303+ for(var i in config)
 304+ eval("window." + i.toString().toUpperCase() + " = " + n++);
 305+ tt_aV.length = n;
 306+}
 307+function tt_Browser()
 308+{
 309+ var n, nv, n6, w3c;
 310+
 311+ n = navigator.userAgent.toLowerCase(),
 312+ nv = navigator.appVersion;
 313+ tt_op = (document.defaultView && typeof(eval("w" + "indow" + "." + "o" + "p" + "er" + "a")) != tt_u);
 314+ tt_ie = n.indexOf("msie") != -1 && document.all && !tt_op;
 315+ if(tt_ie)
 316+ {
 317+ var ieOld = (!document.compatMode || document.compatMode == "BackCompat");
 318+ tt_db = !ieOld ? document.documentElement : (document.body || null);
 319+ if(tt_db)
 320+ tt_ie56 = parseFloat(nv.substring(nv.indexOf("MSIE") + 5)) >= 5.5
 321+ && typeof document.body.style.maxHeight == tt_u;
 322+ }
 323+ else
 324+ {
 325+ tt_db = document.documentElement || document.body ||
 326+ (document.getElementsByTagName ? document.getElementsByTagName("body")[0]
 327+ : null);
 328+ if(!tt_op)
 329+ {
 330+ n6 = document.defaultView && typeof document.defaultView.getComputedStyle != tt_u;
 331+ w3c = !n6 && document.getElementById;
 332+ }
 333+ }
 334+ tt_body = (document.getElementsByTagName ? document.getElementsByTagName("body")[0]
 335+ : (document.body || null));
 336+ if(tt_ie || n6 || tt_op || w3c)
 337+ {
 338+ if(tt_body && tt_db)
 339+ {
 340+ if(document.attachEvent || document.addEventListener)
 341+ return true;
 342+ }
 343+ else
 344+ tt_Err("wz_tooltip.js must be included INSIDE the body section,"
 345+ + " immediately after the opening <body> tag.");
 346+ }
 347+ tt_db = null;
 348+ return false;
 349+}
 350+function tt_MkMainDiv()
 351+{
 352+ // Create the tooltip DIV
 353+ if(tt_body.insertAdjacentHTML)
 354+ tt_body.insertAdjacentHTML("afterBegin", tt_MkMainDivHtm());
 355+ else if(typeof tt_body.innerHTML != tt_u && document.createElement && tt_body.appendChild)
 356+ tt_body.appendChild(tt_MkMainDivDom());
 357+ // FireFox Alzheimer bug
 358+ if(window.tt_GetMainDivRefs && tt_GetMainDivRefs())
 359+ return true;
 360+ tt_db = null;
 361+ return false;
 362+}
 363+function tt_MkMainDivHtm()
 364+{
 365+ return('<div id="WzTtDiV"></div>' +
 366+ (tt_ie56 ? ('<iframe id="WzTtIfRm" src="javascript:false" scrolling="no" frameborder="0" style="filter:Alpha(opacity=0);position:absolute;top:0px;left:0px;display:none;"></iframe>')
 367+ : ''));
 368+}
 369+function tt_MkMainDivDom()
 370+{
 371+ var el = document.createElement("div");
 372+ if(el)
 373+ el.id = "WzTtDiV";
 374+ return el;
 375+}
 376+function tt_GetMainDivRefs()
 377+{
 378+ tt_aElt[0] = tt_GetElt("WzTtDiV");
 379+ if(tt_ie56 && tt_aElt[0])
 380+ {
 381+ tt_aElt[tt_aElt.length - 1] = tt_GetElt("WzTtIfRm");
 382+ if(!tt_aElt[tt_aElt.length - 1])
 383+ tt_aElt[0] = null;
 384+ }
 385+ if(tt_aElt[0])
 386+ {
 387+ var css = tt_aElt[0].style;
 388+
 389+ css.visibility = "hidden";
 390+ css.position = "absolute";
 391+ css.overflow = "hidden";
 392+ return true;
 393+ }
 394+ return false;
 395+}
 396+function tt_ResetMainDiv()
 397+{
 398+ var w = (window.screen && screen.width) ? screen.width : 10000;
 399+
 400+ tt_SetTipPos(-w, 0);
 401+ tt_aElt[0].innerHTML = "";
 402+ tt_aElt[0].style.width = (w - 1) + "px";
 403+}
 404+function tt_IsW3cBox()
 405+{
 406+ var css = tt_aElt[0].style;
 407+
 408+ css.padding = "10px";
 409+ css.width = "40px";
 410+ tt_bBoxOld = (tt_GetDivW(tt_aElt[0]) == 40);
 411+ css.padding = "0px";
 412+ tt_ResetMainDiv();
 413+}
 414+function tt_OpaSupport()
 415+{
 416+ var css = tt_body.style;
 417+
 418+ tt_flagOpa = (typeof(css.filter) != tt_u) ? 1
 419+ : (typeof(css.KhtmlOpacity) != tt_u) ? 2
 420+ : (typeof(css.KHTMLOpacity) != tt_u) ? 3
 421+ : (typeof(css.MozOpacity) != tt_u) ? 4
 422+ : (typeof(css.opacity) != tt_u) ? 5
 423+ : 0;
 424+}
 425+// Ported from http://dean.edwards.name/weblog/2006/06/again/
 426+// (Dean Edwards et al.)
 427+function tt_SetOnloadFnc()
 428+{
 429+ tt_AddEvtFnc(document, "DOMContentLoaded", tt_HideSrcTags);
 430+ tt_AddEvtFnc(window, "load", tt_HideSrcTags);
 431+ if(tt_body.attachEvent)
 432+ tt_body.attachEvent("onreadystatechange",
 433+ function() {
 434+ if(tt_body.readyState == "complete")
 435+ tt_HideSrcTags();
 436+ } );
 437+ if(/WebKit|KHTML/i.test(navigator.userAgent))
 438+ {
 439+ var t = setInterval(function() {
 440+ if(/loaded|complete/.test(document.readyState))
 441+ {
 442+ clearInterval(t);
 443+ tt_HideSrcTags();
 444+ }
 445+ }, 10);
 446+ }
 447+}
 448+function tt_HideSrcTags()
 449+{
 450+ if(!window.tt_HideSrcTags || window.tt_HideSrcTags.done)
 451+ return;
 452+ window.tt_HideSrcTags.done = true;
 453+ if(!tt_HideSrcTagsRecurs(tt_body))
 454+ tt_Err("To enable the capability to convert HTML elements to tooltips,"
 455+ + " you must set TagsToTip in the global tooltip configuration"
 456+ + " to true.");
 457+}
 458+function tt_HideSrcTagsRecurs(dad)
 459+{
 460+ var a, ovr, asT2t;
 461+
 462+ // Walk the DOM tree for tags that have an onmouseover attribute
 463+ // containing a TagToTip('...') call.
 464+ // (.childNodes first since .children is bugous in Safari)
 465+ a = dad.childNodes || dad.children || null;
 466+ for(var i = a ? a.length : 0; i;)
 467+ {--i;
 468+ if(!tt_HideSrcTagsRecurs(a[i]))
 469+ return false;
 470+ ovr = a[i].getAttribute ? a[i].getAttribute("onmouseover")
 471+ : (typeof a[i].onmouseover == "function") ? a[i].onmouseover
 472+ : null;
 473+ if(ovr)
 474+ {
 475+ asT2t = ovr.toString().match(/TagToTip\s*\(\s*'[^'.]+'\s*[\),]/);
 476+ if(asT2t && asT2t.length)
 477+ {
 478+ if(!tt_HideSrcTag(asT2t[0]))
 479+ return false;
 480+ }
 481+ }
 482+ }
 483+ return true;
 484+}
 485+function tt_HideSrcTag(sT2t)
 486+{
 487+ var id, el;
 488+
 489+ // The ID passed to the found TagToTip() call identifies an HTML element
 490+ // to be converted to a tooltip, so hide that element
 491+ id = sT2t.replace(/.+'([^'.]+)'.+/, "$1");
 492+ el = tt_GetElt(id);
 493+ if(el)
 494+ {
 495+ if(tt_Debug && !TagsToTip)
 496+ return false;
 497+ else
 498+ el.style.display = "none";
 499+ }
 500+ else
 501+ tt_Err("Invalid ID\n'" + id + "'\npassed to TagToTip()."
 502+ + " There exists no HTML element with that ID.");
 503+ return true;
 504+}
 505+function tt_Tip(arg, t2t)
 506+{
 507+ if(!tt_db)
 508+ return;
 509+ if(tt_iState)
 510+ tt_Hide();
 511+ if(!tt_Enabled)
 512+ return;
 513+ tt_t2t = t2t;
 514+ if(!tt_ReadCmds(arg))
 515+ return;
 516+ tt_iState = 0x1 | 0x4;
 517+ tt_AdaptConfig1();
 518+ tt_MkTipContent(arg);
 519+ tt_MkTipSubDivs();
 520+ tt_FormatTip();
 521+ tt_bJmpVert = false;
 522+ tt_maxPosX = tt_GetClientW() + tt_scrlX - tt_w - 1;
 523+ tt_maxPosY = tt_GetClientH() + tt_scrlY - tt_h - 1;
 524+ tt_AdaptConfig2();
 525+ // We must fake the first mousemove in order to ensure the tip
 526+ // be immediately shown and positioned
 527+ tt_Move();
 528+ tt_ShowInit();
 529+}
 530+function tt_ReadCmds(a)
 531+{
 532+ var i;
 533+
 534+ // First load the global config values, to initialize also values
 535+ // for which no command has been passed
 536+ i = 0;
 537+ for(var j in config)
 538+ tt_aV[i++] = config[j];
 539+ // Then replace each cached config value for which a command has been
 540+ // passed (ensure the # of command args plus value args be even)
 541+ if(a.length & 1)
 542+ {
 543+ for(i = a.length - 1; i > 0; i -= 2)
 544+ tt_aV[a[i - 1]] = a[i];
 545+ return true;
 546+ }
 547+ tt_Err("Incorrect call of Tip() or TagToTip().\n"
 548+ + "Each command must be followed by a value.");
 549+ return false;
 550+}
 551+function tt_AdaptConfig1()
 552+{
 553+ tt_ExtCallFncs(0, "LoadConfig");
 554+ // Inherit unspecified title formattings from body
 555+ if(!tt_aV[TITLEBGCOLOR].length)
 556+ tt_aV[TITLEBGCOLOR] = tt_aV[BORDERCOLOR];
 557+ if(!tt_aV[TITLEFONTCOLOR].length)
 558+ tt_aV[TITLEFONTCOLOR] = tt_aV[BGCOLOR];
 559+ if(!tt_aV[TITLEFONTFACE].length)
 560+ tt_aV[TITLEFONTFACE] = tt_aV[FONTFACE];
 561+ if(!tt_aV[TITLEFONTSIZE].length)
 562+ tt_aV[TITLEFONTSIZE] = tt_aV[FONTSIZE];
 563+ if(tt_aV[CLOSEBTN])
 564+ {
 565+ // Use title colors for non-specified closebutton colors
 566+ if(!tt_aV[CLOSEBTNCOLORS])
 567+ tt_aV[CLOSEBTNCOLORS] = new Array("", "", "", "");
 568+ for(var i = 4; i;)
 569+ {--i;
 570+ if(!tt_aV[CLOSEBTNCOLORS][i].length)
 571+ tt_aV[CLOSEBTNCOLORS][i] = (i & 1) ? tt_aV[TITLEFONTCOLOR] : tt_aV[TITLEBGCOLOR];
 572+ }
 573+ // Enforce titlebar be shown
 574+ if(!tt_aV[TITLE].length)
 575+ tt_aV[TITLE] = " ";
 576+ }
 577+ // Circumvents broken display of images and fade-in flicker in Geckos < 1.8
 578+ if(tt_aV[OPACITY] == 100 && typeof tt_aElt[0].style.MozOpacity != tt_u && !Array.every)
 579+ tt_aV[OPACITY] = 99;
 580+ // Smartly shorten the delay for fade-in tooltips
 581+ if(tt_aV[FADEIN] && tt_flagOpa && tt_aV[DELAY] > 100)
 582+ tt_aV[DELAY] = Math.max(tt_aV[DELAY] - tt_aV[FADEIN], 100);
 583+}
 584+function tt_AdaptConfig2()
 585+{
 586+ if(tt_aV[CENTERMOUSE])
 587+ tt_aV[OFFSETX] -= ((tt_w - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0)) >> 1);
 588+}
 589+// Expose content globally so extensions can modify it
 590+function tt_MkTipContent(a)
 591+{
 592+ if(tt_t2t)
 593+ {
 594+ if(tt_aV[COPYCONTENT])
 595+ tt_sContent = tt_t2t.innerHTML;
 596+ else
 597+ tt_sContent = "";
 598+ }
 599+ else
 600+ tt_sContent = a[0];
 601+ tt_ExtCallFncs(0, "CreateContentString");
 602+}
 603+function tt_MkTipSubDivs()
 604+{
 605+ var sCss = 'position:relative;margin:0px;padding:0px;border-width:0px;left:0px;top:0px;line-height:normal;width:auto;',
 606+ sTbTrTd = ' cellspacing=0 cellpadding=0 border=0 style="' + sCss + '"><tbody style="' + sCss + '"><tr><td ';
 607+
 608+ tt_aElt[0].innerHTML =
 609+ (''
 610+ + (tt_aV[TITLE].length ?
 611+ ('<div id="WzTiTl" style="position:relative;z-index:1;">'
 612+ + '<table id="WzTiTlTb"' + sTbTrTd + 'id="WzTiTlI" style="' + sCss + '">'
 613+ + tt_aV[TITLE]
 614+ + '</td>'
 615+ + (tt_aV[CLOSEBTN] ?
 616+ ('<td align="right" style="' + sCss
 617+ + 'text-align:right;">'
 618+ + '<span id="WzClOsE" style="padding-left:2px;padding-right:2px;'
 619+ + 'cursor:' + (tt_ie ? 'hand' : 'pointer')
 620+ + ';" onmouseover="tt_OnCloseBtnOver(1)" onmouseout="tt_OnCloseBtnOver(0)" onclick="tt_HideInit()">'
 621+ + tt_aV[CLOSEBTNTEXT]
 622+ + '</span></td>')
 623+ : '')
 624+ + '</tr></tbody></table></div>')
 625+ : '')
 626+ + '<div id="WzBoDy" style="position:relative;z-index:0;">'
 627+ + '<table' + sTbTrTd + 'id="WzBoDyI" style="' + sCss + '">'
 628+ + tt_sContent
 629+ + '</td></tr></tbody></table></div>'
 630+ + (tt_aV[SHADOW]
 631+ ? ('<div id="WzTtShDwR" style="position:absolute;overflow:hidden;"></div>'
 632+ + '<div id="WzTtShDwB" style="position:relative;overflow:hidden;"></div>')
 633+ : '')
 634+ );
 635+ tt_GetSubDivRefs();
 636+ // Convert DOM node to tip
 637+ if(tt_t2t && !tt_aV[COPYCONTENT])
 638+ {
 639+ // Store the tag's parent element so we can restore that DOM branch
 640+ // once the tooltip is hidden
 641+ tt_t2tDad = tt_t2t.parentNode || tt_t2t.parentElement || tt_t2t.offsetParent || null;
 642+ if(tt_t2tDad)
 643+ {
 644+ tt_MovDomNode(tt_t2t, tt_t2tDad, tt_aElt[6]);
 645+ tt_t2t.style.display = "block";
 646+ }
 647+ }
 648+ tt_ExtCallFncs(0, "SubDivsCreated");
 649+}
 650+function tt_GetSubDivRefs()
 651+{
 652+ var aId = new Array("WzTiTl", "WzTiTlTb", "WzTiTlI", "WzClOsE", "WzBoDy", "WzBoDyI", "WzTtShDwB", "WzTtShDwR");
 653+
 654+ for(var i = aId.length; i; --i)
 655+ tt_aElt[i] = tt_GetElt(aId[i - 1]);
 656+}
 657+function tt_FormatTip()
 658+{
 659+ var css, w, iOffY, iOffSh;
 660+
 661+ //--------- Title DIV ----------
 662+ if(tt_aV[TITLE].length)
 663+ {
 664+ css = tt_aElt[1].style;
 665+ css.background = tt_aV[TITLEBGCOLOR];
 666+ css.paddingTop = (tt_aV[CLOSEBTN] ? 2 : 0) + "px";
 667+ css.paddingBottom = "1px";
 668+ css.paddingLeft = css.paddingRight = tt_aV[PADDING] + "px";
 669+ css = tt_aElt[3].style;
 670+ css.color = tt_aV[TITLEFONTCOLOR];
 671+ css.fontFamily = tt_aV[TITLEFONTFACE];
 672+ css.fontSize = tt_aV[TITLEFONTSIZE];
 673+ css.fontWeight = "bold";
 674+ css.textAlign = tt_aV[TITLEALIGN];
 675+ // Close button DIV
 676+ if(tt_aElt[4])
 677+ {
 678+ css.paddingRight = (tt_aV[PADDING] << 1) + "px";
 679+ css = tt_aElt[4].style;
 680+ css.background = tt_aV[CLOSEBTNCOLORS][0];
 681+ css.color = tt_aV[CLOSEBTNCOLORS][1];
 682+ css.fontFamily = tt_aV[TITLEFONTFACE];
 683+ css.fontSize = tt_aV[TITLEFONTSIZE];
 684+ css.fontWeight = "bold";
 685+ }
 686+ if(tt_aV[WIDTH] > 0)
 687+ tt_w = tt_aV[WIDTH] + ((tt_aV[PADDING] + tt_aV[BORDERWIDTH]) << 1);
 688+ else
 689+ {
 690+ tt_w = tt_GetDivW(tt_aElt[3]) + tt_GetDivW(tt_aElt[4]);
 691+ // Some spacing between title DIV and closebutton
 692+ if(tt_aElt[4])
 693+ tt_w += tt_aV[PADDING];
 694+ }
 695+ // Ensure the top border of the body DIV be covered by the title DIV
 696+ iOffY = -tt_aV[BORDERWIDTH];
 697+ }
 698+ else
 699+ {
 700+ tt_w = 0;
 701+ iOffY = 0;
 702+ }
 703+
 704+ //-------- Body DIV ------------
 705+ css = tt_aElt[5].style;
 706+ css.top = iOffY + "px";
 707+ if(tt_aV[BORDERWIDTH])
 708+ {
 709+ css.borderColor = tt_aV[BORDERCOLOR];
 710+ css.borderStyle = tt_aV[BORDERSTYLE];
 711+ css.borderWidth = tt_aV[BORDERWIDTH] + "px";
 712+ }
 713+ if(tt_aV[BGCOLOR].length)
 714+ css.background = tt_aV[BGCOLOR];
 715+ if(tt_aV[BGIMG].length)
 716+ css.backgroundImage = "url(" + tt_aV[BGIMG] + ")";
 717+ css.padding = tt_aV[PADDING] + "px";
 718+ css.textAlign = tt_aV[TEXTALIGN];
 719+ // TD inside body DIV
 720+ css = tt_aElt[6].style;
 721+ css.color = tt_aV[FONTCOLOR];
 722+ css.fontFamily = tt_aV[FONTFACE];
 723+ css.fontSize = tt_aV[FONTSIZE];
 724+ css.fontWeight = tt_aV[FONTWEIGHT];
 725+ css.background = "";
 726+ css.textAlign = tt_aV[TEXTALIGN];
 727+ if(tt_aV[WIDTH] > 0)
 728+ w = tt_aV[WIDTH] + ((tt_aV[PADDING] + tt_aV[BORDERWIDTH]) << 1);
 729+ else
 730+ // We measure the width of the body's inner TD, because some browsers
 731+ // expand the width of the container and outer body DIV to 100%
 732+ w = tt_GetDivW(tt_aElt[6]) + ((tt_aV[PADDING] + tt_aV[BORDERWIDTH]) << 1);
 733+ if(w > tt_w)
 734+ tt_w = w;
 735+
 736+ //--------- Shadow DIVs ------------
 737+ if(tt_aV[SHADOW])
 738+ {
 739+ tt_w += tt_aV[SHADOWWIDTH];
 740+ iOffSh = Math.floor((tt_aV[SHADOWWIDTH] * 4) / 3);
 741+ // Bottom shadow
 742+ css = tt_aElt[7].style;
 743+ css.top = iOffY + "px";
 744+ css.left = iOffSh + "px";
 745+ css.width = (tt_w - iOffSh - tt_aV[SHADOWWIDTH]) + "px";
 746+ css.height = tt_aV[SHADOWWIDTH] + "px";
 747+ css.background = tt_aV[SHADOWCOLOR];
 748+ // Right shadow
 749+ css = tt_aElt[8].style;
 750+ css.top = iOffSh + "px";
 751+ css.left = (tt_w - tt_aV[SHADOWWIDTH]) + "px";
 752+ css.width = tt_aV[SHADOWWIDTH] + "px";
 753+ css.background = tt_aV[SHADOWCOLOR];
 754+ }
 755+ else
 756+ iOffSh = 0;
 757+
 758+ //-------- Container DIV -------
 759+ tt_SetTipOpa(tt_aV[FADEIN] ? 0 : tt_aV[OPACITY]);
 760+ tt_FixSize(iOffY, iOffSh);
 761+}
 762+// Fixate the size so it can't dynamically change while the tooltip is moving.
 763+function tt_FixSize(iOffY, iOffSh)
 764+{
 765+ var wIn, wOut, i;
 766+
 767+ tt_aElt[0].style.width = tt_w + "px";
 768+ tt_aElt[0].style.pixelWidth = tt_w;
 769+ wOut = tt_w - ((tt_aV[SHADOW]) ? tt_aV[SHADOWWIDTH] : 0);
 770+ // Body
 771+ wIn = wOut;
 772+ if(!tt_bBoxOld)
 773+ wIn -= ((tt_aV[PADDING] + tt_aV[BORDERWIDTH]) << 1);
 774+ tt_aElt[5].style.width = wIn + "px";
 775+ // Title
 776+ if(tt_aElt[1])
 777+ {
 778+ wIn = wOut - (tt_aV[PADDING] << 1);
 779+ if(!tt_bBoxOld)
 780+ wOut = wIn;
 781+ tt_aElt[1].style.width = wOut + "px";
 782+ tt_aElt[2].style.width = wIn + "px";
 783+ }
 784+ tt_h = tt_GetDivH(tt_aElt[0]) + iOffY;
 785+ // Right shadow
 786+ if(tt_aElt[8])
 787+ tt_aElt[8].style.height = (tt_h - iOffSh) + "px";
 788+ i = tt_aElt.length - 1;
 789+ if(tt_aElt[i])
 790+ {
 791+ tt_aElt[i].style.width = tt_w + "px";
 792+ tt_aElt[i].style.height = tt_h + "px";
 793+ }
 794+}
 795+function tt_DeAlt(el)
 796+{
 797+ var aKid;
 798+
 799+ if(el.alt)
 800+ el.alt = "";
 801+ if(el.title)
 802+ el.title = "";
 803+ aKid = el.childNodes || el.children || null;
 804+ if(aKid)
 805+ {
 806+ for(var i = aKid.length; i;)
 807+ tt_DeAlt(aKid[--i]);
 808+ }
 809+}
 810+// This hack removes the annoying native tooltips over links in Opera
 811+function tt_OpDeHref(el)
 812+{
 813+ if(!tt_op)
 814+ return;
 815+ if(tt_elDeHref)
 816+ tt_OpReHref();
 817+ while(el)
 818+ {
 819+ if(el.hasAttribute("href"))
 820+ {
 821+ el.t_href = el.getAttribute("href");
 822+ el.t_stats = window.status;
 823+ el.removeAttribute("href");
 824+ el.style.cursor = "hand";
 825+ tt_AddEvtFnc(el, "mousedown", tt_OpReHref);
 826+ window.status = el.t_href;
 827+ tt_elDeHref = el;
 828+ break;
 829+ }
 830+ el = el.parentElement;
 831+ }
 832+}
 833+function tt_ShowInit()
 834+{
 835+ tt_tShow.Timer("tt_Show()", tt_aV[DELAY], true);
 836+ if(tt_aV[CLICKCLOSE])
 837+ tt_AddEvtFnc(document, "mouseup", tt_HideInit);
 838+}
 839+function tt_OverInit(e)
 840+{
 841+ tt_over = e.target || e.srcElement;
 842+ tt_DeAlt(tt_over);
 843+ tt_OpDeHref(tt_over);
 844+ tt_AddRemOutFnc(true);
 845+}
 846+function tt_Show()
 847+{
 848+ var css = tt_aElt[0].style;
 849+
 850+ // Override the z-index of the topmost wz_dragdrop.js D&D item
 851+ css.zIndex = Math.max((window.dd && dd.z) ? (dd.z + 2) : 0, 1010);
 852+ if(tt_aV[STICKY] || !tt_aV[FOLLOWMOUSE])
 853+ tt_iState &= ~0x4;
 854+ if(tt_aV[DURATION] > 0)
 855+ tt_tDurt.Timer("tt_HideInit()", tt_aV[DURATION], true);
 856+ tt_ExtCallFncs(0, "Show")
 857+ css.visibility = "visible";
 858+ tt_iState |= 0x2;
 859+ if(tt_aV[FADEIN])
 860+ tt_Fade(0, 0, tt_aV[OPACITY], Math.round(tt_aV[FADEIN] / tt_aV[FADEINTERVAL]));
 861+ tt_ShowIfrm();
 862+}
 863+function tt_ShowIfrm()
 864+{
 865+ if(tt_ie56)
 866+ {
 867+ var ifrm = tt_aElt[tt_aElt.length - 1];
 868+ if(ifrm)
 869+ {
 870+ var css = ifrm.style;
 871+ css.zIndex = tt_aElt[0].style.zIndex - 1;
 872+ css.display = "block";
 873+ }
 874+ }
 875+}
 876+function tt_Move(e)
 877+{
 878+ e = window.event || e;
 879+ if(e)
 880+ {
 881+ tt_musX = tt_GetEvtX(e);
 882+ tt_musY = tt_GetEvtY(e);
 883+ }
 884+ if(tt_iState)
 885+ {
 886+ if(!tt_over && e)
 887+ tt_OverInit(e);
 888+ if(tt_iState & 0x4)
 889+ {
 890+ // Protect some browsers against jam of mousemove events
 891+ if(!tt_op && !tt_ie)
 892+ {
 893+ if(tt_bWait)
 894+ return;
 895+ tt_bWait = true;
 896+ tt_tWaitMov.Timer("tt_bWait = false;", 1, true);
 897+ }
 898+ if(tt_aV[FIX])
 899+ {
 900+ tt_iState &= ~0x4;
 901+ tt_SetTipPos(tt_aV[FIX][0], tt_aV[FIX][1]);
 902+ }
 903+ else if(!tt_ExtCallFncs(e, "MoveBefore"))
 904+ tt_SetTipPos(tt_PosX(), tt_PosY());
 905+ tt_ExtCallFncs([tt_musX, tt_musY], "MoveAfter")
 906+ }
 907+ }
 908+}
 909+function tt_PosX()
 910+{
 911+ var x;
 912+
 913+ x = tt_musX;
 914+ if(tt_aV[LEFT])
 915+ x -= tt_w + tt_aV[OFFSETX] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0);
 916+ else
 917+ x += tt_aV[OFFSETX];
 918+ // Prevent tip from extending past right/left clientarea boundary
 919+ if(x > tt_maxPosX)
 920+ x = tt_maxPosX;
 921+ return((x < tt_scrlX) ? tt_scrlX : x);
 922+}
 923+function tt_PosY()
 924+{
 925+ var y;
 926+
 927+ // Apply some hysteresis after the tip has snapped to the other side of the
 928+ // mouse. In case of insufficient space above and below the mouse, we place
 929+ // the tip below.
 930+ if(tt_aV[ABOVE] && (!tt_bJmpVert || tt_CalcPosYAbove() >= tt_scrlY + 16))
 931+ y = tt_DoPosYAbove();
 932+ else if(!tt_aV[ABOVE] && tt_bJmpVert && tt_CalcPosYBelow() > tt_maxPosY - 16)
 933+ y = tt_DoPosYAbove();
 934+ else
 935+ y = tt_DoPosYBelow();
 936+ // Snap to other side of mouse if tip would extend past window boundary
 937+ if(y > tt_maxPosY)
 938+ y = tt_DoPosYAbove();
 939+ if(y < tt_scrlY)
 940+ y = tt_DoPosYBelow();
 941+ return y;
 942+}
 943+function tt_DoPosYBelow()
 944+{
 945+ tt_bJmpVert = tt_aV[ABOVE];
 946+ return tt_CalcPosYBelow();
 947+}
 948+function tt_DoPosYAbove()
 949+{
 950+ tt_bJmpVert = !tt_aV[ABOVE];
 951+ return tt_CalcPosYAbove();
 952+}
 953+function tt_CalcPosYBelow()
 954+{
 955+ return(tt_musY + tt_aV[OFFSETY]);
 956+}
 957+function tt_CalcPosYAbove()
 958+{
 959+ var dy = tt_aV[OFFSETY] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0);
 960+ if(tt_aV[OFFSETY] > 0 && dy <= 0)
 961+ dy = 1;
 962+ return(tt_musY - tt_h - dy);
 963+}
 964+function tt_OnOut()
 965+{
 966+ tt_AddRemOutFnc(false);
 967+ if(!(tt_aV[STICKY] && (tt_iState & 0x2)))
 968+ tt_HideInit();
 969+}
 970+function tt_HideInit()
 971+{
 972+ tt_ExtCallFncs(0, "HideInit");
 973+ tt_iState &= ~0x4;
 974+ if(tt_flagOpa && tt_aV[FADEOUT])
 975+ {
 976+ tt_tFade.EndTimer();
 977+ if(tt_opa)
 978+ {
 979+ var n = Math.round(tt_aV[FADEOUT] / (tt_aV[FADEINTERVAL] * (tt_aV[OPACITY] / tt_opa)));
 980+ tt_Fade(tt_opa, tt_opa, 0, n);
 981+ return;
 982+ }
 983+ }
 984+ tt_tHide.Timer("tt_Hide();", 1, false);
 985+}
 986+function tt_OpReHref()
 987+{
 988+ if(tt_elDeHref)
 989+ {
 990+ tt_elDeHref.setAttribute("href", tt_elDeHref.t_href);
 991+ tt_RemEvtFnc(tt_elDeHref, "mousedown", tt_OpReHref);
 992+ window.status = tt_elDeHref.t_stats;
 993+ tt_elDeHref = null;
 994+ }
 995+}
 996+function tt_Fade(a, now, z, n)
 997+{
 998+ if(n)
 999+ {
 1000+ now += Math.round((z - now) / n);
 1001+ if((z > a) ? (now >= z) : (now <= z))
 1002+ now = z;
 1003+ else
 1004+ tt_tFade.Timer("tt_Fade("
 1005+ + a + "," + now + "," + z + "," + (n - 1)
 1006+ + ")",
 1007+ tt_aV[FADEINTERVAL],
 1008+ true);
 1009+ }
 1010+ now ? tt_SetTipOpa(now) : tt_Hide();
 1011+}
 1012+// To circumvent the opacity nesting flaws of IE, we set the opacity
 1013+// for each sub-DIV separately, rather than for the container DIV.
 1014+function tt_SetTipOpa(opa)
 1015+{
 1016+ tt_SetOpa(tt_aElt[5].style, opa);
 1017+ if(tt_aElt[1])
 1018+ tt_SetOpa(tt_aElt[1].style, opa);
 1019+ if(tt_aV[SHADOW])
 1020+ {
 1021+ opa = Math.round(opa * 0.8);
 1022+ tt_SetOpa(tt_aElt[7].style, opa);
 1023+ tt_SetOpa(tt_aElt[8].style, opa);
 1024+ }
 1025+}
 1026+function tt_OnCloseBtnOver(iOver)
 1027+{
 1028+ var css = tt_aElt[4].style;
 1029+
 1030+ iOver <<= 1;
 1031+ css.background = tt_aV[CLOSEBTNCOLORS][iOver];
 1032+ css.color = tt_aV[CLOSEBTNCOLORS][iOver + 1];
 1033+}
 1034+function tt_Int(x)
 1035+{
 1036+ var y;
 1037+
 1038+ return(isNaN(y = parseInt(x)) ? 0 : y);
 1039+}
 1040+// Adds or removes the document.mousemove or HoveredElem.mouseout handler
 1041+// conveniently. Keeps track of those handlers to prevent them from being
 1042+// set or removed redundantly.
 1043+function tt_AddRemOutFnc(bAdd)
 1044+{
 1045+ var PSet = bAdd ? tt_AddEvtFnc : tt_RemEvtFnc;
 1046+
 1047+ if(bAdd != tt_AddRemOutFnc.bOn)
 1048+ {
 1049+ PSet(tt_over, "mouseout", tt_OnOut);
 1050+ tt_AddRemOutFnc.bOn = bAdd;
 1051+ if(!bAdd)
 1052+ tt_OpReHref();
 1053+ }
 1054+}
 1055+tt_AddRemOutFnc.bOn = false;
 1056+Number.prototype.Timer = function(s, iT, bUrge)
 1057+{
 1058+ if(!this.value || bUrge)
 1059+ this.value = window.setTimeout(s, iT);
 1060+}
 1061+Number.prototype.EndTimer = function()
 1062+{
 1063+ if(this.value)
 1064+ {
 1065+ window.clearTimeout(this.value);
 1066+ this.value = 0;
 1067+ }
 1068+}
 1069+function tt_SetOpa(css, opa)
 1070+{
 1071+ tt_opa = opa;
 1072+ if(tt_flagOpa == 1)
 1073+ {
 1074+ // Hack for bugs of IE:
 1075+ // A DIV cannot be made visible in a single step if an opacity < 100
 1076+ // has been applied while the DIV was hidden.
 1077+ // Moreover, in IE6, applying an opacity < 100 has no effect if the
 1078+ // concerned element has no layout (position, size, zoom, ...).
 1079+ if(opa < 100)
 1080+ {
 1081+ var bVis = css.visibility != "hidden";
 1082+ css.zoom = "100%";
 1083+ if(!bVis)
 1084+ css.visibility = "visible";
 1085+ css.filter = "alpha(opacity=" + opa + ")";
 1086+ if(!bVis)
 1087+ css.visibility = "hidden";
 1088+ }
 1089+ else
 1090+ css.filter = "";
 1091+ }
 1092+ else
 1093+ {
 1094+ opa /= 100.0;
 1095+ switch(tt_flagOpa)
 1096+ {
 1097+ case 2:
 1098+ css.KhtmlOpacity = opa; break;
 1099+ case 3:
 1100+ css.KHTMLOpacity = opa; break;
 1101+ case 4:
 1102+ css.MozOpacity = opa; break;
 1103+ case 5:
 1104+ css.opacity = opa; break;
 1105+ }
 1106+ }
 1107+}
 1108+function tt_MovDomNode(el, dadFrom, dadTo)
 1109+{
 1110+ if(dadFrom)
 1111+ dadFrom.removeChild(el);
 1112+ if(dadTo)
 1113+ dadTo.appendChild(el);
 1114+}
 1115+function tt_Err(sErr)
 1116+{
 1117+ if(tt_Debug)
 1118+ alert("Tooltip Script Error Message:\n\n" + sErr);
 1119+}
 1120+
 1121+//=========== DEALING WITH EXTENSIONS ==============//
 1122+function tt_ExtCmdEnum()
 1123+{
 1124+ var s;
 1125+
 1126+ // Add new command(s) to the commands enum
 1127+ for(var i in config)
 1128+ {
 1129+ s = "window." + i.toString().toUpperCase();
 1130+ if(eval("typeof(" + s + ") == tt_u"))
 1131+ {
 1132+ eval(s + " = " + tt_aV.length);
 1133+ tt_aV[tt_aV.length] = null;
 1134+ }
 1135+ }
 1136+}
 1137+function tt_ExtCallFncs(arg, sFnc)
 1138+{
 1139+ var b = false;
 1140+ for(var i = tt_aExt.length; i;)
 1141+ {--i;
 1142+ var fnc = tt_aExt[i]["On" + sFnc];
 1143+ // Call the method the extension has defined for this event
 1144+ if(fnc && fnc(arg))
 1145+ b = true;
 1146+ }
 1147+ return b;
 1148+}
 1149+
 1150+tt_Init();
Index: trunk/extensions/SemanticNotifyMe/scripts/NotifyMe/NotifyHelper.js
@@ -0,0 +1,564 @@
 2+/** *****************************************************************************
 3+* NotifyMe helper for Semantic MediaWiki
 4+* Developed by Dch <hehehu@gmail.com>
 5+*
 6+* NotifyHelper.js
 7+* Manages major functionalities and GUI of the User Notification Interface
 8+* @author Dch [hehehu@gmail.com]
 9+*/
 10+
 11+var notifyhelper = null;
 12+
 13+var NotifyHelper = Class.create();
 14+NotifyHelper.prototype = {
 15+
 16+/**
 17+* Initialize the NotifyHelper object and all variables
 18+*/
 19+initialize:function(){
 20+ this.pendingElement = null;
 21+},
 22+
 23+/**
 24+* Called whenever notify me manager is minimized or maximized
 25+*/
 26+switchlayout:function(){
 27+ if($("layoutcontent").style.display == "none"){
 28+ $("layoutcontent").style.display = "";
 29+ $("layouttitle-link").removeClassName("plusminus");
 30+ $("layouttitle-link").addClassName("minusplus");
 31+ }
 32+ else {
 33+ $("layoutcontent").style.display = "none";
 34+ $("layouttitle-link").removeClassName("minusplus");
 35+ $("layouttitle-link").addClassName("plusminus");
 36+ }
 37+},
 38+/**
 39+* Called whenever notify me query is minimized or maximized
 40+*/
 41+switchquery:function(){
 42+ if($("querycontent").style.display == "none"){
 43+ $("querycontent").style.display = "";
 44+ $("querytitle-link").removeClassName("plusminus");
 45+ $("querytitle-link").addClassName("minusplus");
 46+ }
 47+ else {
 48+ $("querycontent").style.display = "none";
 49+ $("querytitle-link").removeClassName("minusplus");
 50+ $("querytitle-link").addClassName("plusminus");
 51+ }
 52+},
 53+
 54+/**
 55+* Executes a save
 56+*/
 57+doSaveToNotify:function(){
 58+ /*STARTLOG*/
 59+ if(window.smwhgLogger){
 60+ smwhgLogger.log("Save Notify","NM","save_notify");
 61+ }
 62+ /*ENDLOG*/
 63+
 64+ $('shade').toggle();
 65+ if(this.pendingElement)
 66+ this.pendingElement.hide();
 67+ this.pendingElement = new OBPendingIndicator($('shade'));
 68+ this.pendingElement.show();
 69+
 70+ if($('nmqname').value=="") {
 71+ var request = Array();
 72+ request.responseText = nmLanguage.getMessage('NM_EMPTY_NOTIFYNAME');
 73+ this.saveNotify(request);
 74+ }
 75+ else if ($('nmquery').value){ //only do this if the query is not empty
 76+ var params = $('nmquery').value.replace(/&/gm, "&amp;").replace(/,/gm, "&comma;") + ",";
 77+ params += ($('nmqrall').checked ? 1 : 0) + ",";
 78+ params += ($('nmqsall').checked ? 1 : 0) + ",";
 79+ params += $('nmqname').value.replace(/&/gm, "&amp;").replace(/,/gm, "&comma;") + ",";
 80+ params += ($('nmd_new') ? $('nmd_new').value : "");
 81+ sajax_do_call('smwf_nm_NotifyAccess', ["addNotify", params], this.saveNotify.bind(this));
 82+ }
 83+ else { // query is empty
 84+ var request = Array();
 85+ request.responseText = nmLanguage.getMessage('NM_EMPTY_QUERY');
 86+ this.saveNotify(request);
 87+ }
 88+},
 89+
 90+/**
 91+* Displays the preview created by the server
 92+* @param request Request of AJAX call
 93+*/
 94+saveNotify:function(request){
 95+ this.pendingElement.hide();
 96+ var s = request.responseText;
 97+ if(s.substring(0,1)=="1") {
 98+ var enabled = (s.substring(1,2)=='1');
 99+ s = s.substring(2);
 100+ var i = s.indexOf(",");
 101+ var nid = s.substring(0,i);
 102+ this.addNotifyToTable(nid, enabled);
 103+ alert("Notify create successfully!" + s.substring(i+1));
 104+ } else alert(s);
 105+ $('shade').toggle();
 106+},
 107+addNotifyToTable:function(nid, enabled){
 108+ var ntr = document.createElement("tr");
 109+ var td = document.createElement("td");
 110+ ntr.appendChild(td);
 111+ var item = document.createElement("input");
 112+ td.appendChild(item);
 113+ item.type = "checkbox";
 114+ item.value = nid;
 115+ item.name = "nmdel";
 116+
 117+ td = document.createElement("td");
 118+ ntr.appendChild(td);
 119+ item = document.createElement("a");
 120+ td.appendChild(item);
 121+ item.href = "index.php?title=Special:NotifyMe&feed=rss&nid=" + nid;
 122+ item.target = "_blank";
 123+ item.innerHTML = $('nmqname').value;
 124+
 125+ td = document.createElement("td");
 126+ ntr.appendChild(td);
 127+ td.innerHTML = $('nmquery').value.replace(/\n/g, "<br/>");
 128+
 129+ td = document.createElement("td");
 130+ ntr.appendChild(td);
 131+ item = document.createElement("input");
 132+ td.appendChild(item);
 133+ item.type = "checkbox";
 134+ item.value = nid;
 135+ item.name = "nmall";
 136+ item.checked = $('nmqrall').checked;
 137+
 138+ td = document.createElement("td");
 139+ ntr.appendChild(td);
 140+ item = document.createElement("input");
 141+ td.appendChild(item);
 142+ item.type = "checkbox";
 143+ item.value = nid;
 144+ item.name = "nmsall";
 145+ item.checked = $('nmqsall').checked;
 146+
 147+ td = document.createElement("td");
 148+ ntr.appendChild(td);
 149+ item = document.createElement("input");
 150+ td.appendChild(item);
 151+ item.type = "checkbox";
 152+ item.value = nid;
 153+ item.name = "nmenable";
 154+ item.id = "nmenable_" + nid;
 155+ item.checked = enabled;
 156+
 157+ if($('nmd_new')) {
 158+ td = document.createElement("td");
 159+ ntr.appendChild(td);
 160+ item = document.createElement("input");
 161+ td.appendChild(item);
 162+ item.type = "text";
 163+ item.value = $('nmd_new').value;
 164+ item.name = "nmdelegate";
 165+ item.id = "nmd_" + nid;
 166+
 167+ item = document.createElement("div");
 168+ td.appendChild(item);
 169+ item.class = "page_name_auto_complete";
 170+ item.id = "nmdiv_" + nid;
 171+ }
 172+
 173+ $('nmtoolbar').parentNode.insertBefore(ntr, $('nmtoolbar'));
 174+ attachAutocompleteToField("nmd_" + nid);
 175+},
 176+
 177+doUpdateMail:function() {
 178+ /*STARTLOG*/
 179+ if(window.smwhgLogger){
 180+ smwhgLogger.log("update Notify Me mail setting ","NM","update_mail");
 181+ }
 182+ /*ENDLOG*/
 183+
 184+ $('shade').toggle();
 185+ if(this.pendingElement)
 186+ this.pendingElement.hide();
 187+ this.pendingElement = new OBPendingIndicator($('shade'));
 188+ this.pendingElement.show();
 189+
 190+ var params = ($('nmemail').checked ? 1 : 0);
 191+ sajax_do_call('smwf_nm_NotifyAccess', ["updateMail", params], this.updateMail.bind(this));
 192+},
 193+updateMail:function(request){
 194+ this.pendingElement.hide();
 195+ alert(request.responseText);
 196+ $('shade').toggle();
 197+},
 198+
 199+resetQuery:function(){
 200+ $('nmquery').value = "";
 201+ $('nmqname').value = "";
 202+ $('nmd_new').value = "";
 203+ $('nmqrall').checked = true;
 204+ $('nmqsall').checked = false;
 205+},
 206+
 207+/**
 208+* Gets all display parameters and the full ask syntax to perform an ajax call
 209+* which will create the preview
 210+*/
 211+previewQuery:function(){
 212+
 213+ /*STARTLOG*/
 214+ if(window.smwhgLogger){
 215+ smwhgLogger.log("Preview Query","NM","query_preview");
 216+ }
 217+ /*ENDLOG*/
 218+ $('shade').toggle();
 219+ if(this.pendingElement)
 220+ this.pendingElement.hide();
 221+ this.pendingElement = new OBPendingIndicator($('shade'));
 222+ this.pendingElement.show();
 223+ if ($('nmquery').value){ //only do this if the query is not empty
 224+ sajax_do_call('smwf_nm_NotifyAccess', ["getQueryResult", $('nmquery').value], this.openPreview.bind(this));
 225+ }
 226+ else { // query is empty
 227+ var request = Array();
 228+ request.responseText = nmLanguage.getMessage('NM_EMPTY_QUERY');
 229+ this.openPreview(request);
 230+ }
 231+},
 232+/**
 233+* Displays the preview created by the server
 234+* @param request Request of AJAX call
 235+*/
 236+openPreview:function(request){
 237+ this.pendingElement.hide();
 238+ $('fullpreviewbox').toggle();
 239+ $('fullpreview').innerHTML = request.responseText;
 240+},
 241+
 242+
 243+updateStates:function(){
 244+ /*STARTLOG*/
 245+ if(window.smwhgLogger){
 246+ smwhgLogger.log("Update States","NM","update_states");
 247+ }
 248+ /*ENDLOG*/
 249+
 250+ $('shade').toggle();
 251+ if(this.pendingElement)
 252+ this.pendingElement.hide();
 253+ this.pendingElement = new OBPendingIndicator($('shade'));
 254+ this.pendingElement.show();
 255+
 256+ var params = "";
 257+ var nodes = $A(document.getElementsByName('nmenable'));
 258+ var dNodes = nodes.select(function(node) {
 259+ return node.checked;
 260+ });
 261+ dNodes.each(function(node) {
 262+ params += node.value + ',';
 263+ });
 264+ sajax_do_call('smwf_nm_NotifyAccess', ["updateStates", params], this.updateStatesDone.bind(this));
 265+},
 266+/**
 267+* @param request Request of AJAX call
 268+*/
 269+updateStatesDone:function(request){
 270+ this.pendingElement.hide();
 271+ var s = request.responseText;
 272+ if(s.substring(0,1)=="0") {
 273+ s = s.substring(1);
 274+ var i = s.indexOf("|");
 275+ var nids = s.substring(0,i).split(",");
 276+ for(j=nids.length-2;j>=0;--j) {
 277+ $('nmenable_'+nids[j]).checked = false;
 278+ }
 279+ alert("Notify create failed!" + s.substring(i+1));
 280+ } else alert(s);
 281+ $('shade').toggle();
 282+},
 283+
 284+updateReportAll:function(){
 285+ /*STARTLOG*/
 286+ if(window.smwhgLogger){
 287+ smwhgLogger.log("Update Notifications report all","NM","update_reportall");
 288+ }
 289+ /*ENDLOG*/
 290+
 291+ $('shade').toggle();
 292+ if(this.pendingElement)
 293+ this.pendingElement.hide();
 294+ this.pendingElement = new OBPendingIndicator($('shade'));
 295+ this.pendingElement.show();
 296+
 297+ var params = "";
 298+ var nodes = $A(document.getElementsByName('nmall'));
 299+ var dNodes = nodes.select(function(node) {
 300+ return node.checked;
 301+ });
 302+ dNodes.each(function(node) {
 303+ params += node.value + ',';
 304+ });
 305+ sajax_do_call('smwf_nm_NotifyAccess', ["updateReportAll", params], this.updateDone.bind(this));
 306+},
 307+
 308+updateShowAll:function(){
 309+ /*STARTLOG*/
 310+ if(window.smwhgLogger){
 311+ smwhgLogger.log("Update Notifications show all","NM","update_showall");
 312+ }
 313+ /*ENDLOG*/
 314+
 315+ $('shade').toggle();
 316+ if(this.pendingElement)
 317+ this.pendingElement.hide();
 318+ this.pendingElement = new OBPendingIndicator($('shade'));
 319+ this.pendingElement.show();
 320+
 321+ var params = "";
 322+ var nodes = $A(document.getElementsByName('nmsall'));
 323+ var dNodes = nodes.select(function(node) {
 324+ return node.checked;
 325+ });
 326+ dNodes.each(function(node) {
 327+ params += node.value + ',';
 328+ });
 329+ sajax_do_call('smwf_nm_NotifyAccess', ["updateShowAll", params], this.updateDone.bind(this));
 330+},
 331+/**
 332+* @param request Request of AJAX call
 333+*/
 334+updateDone:function(request){
 335+ this.pendingElement.hide();
 336+ alert(request.responseText);
 337+ $('shade').toggle();
 338+},
 339+
 340+deleteNotify:function(){
 341+ /*STARTLOG*/
 342+ if(window.smwhgLogger){
 343+ smwhgLogger.log("Delete Notifications","NM","delete_notify");
 344+ }
 345+ /*ENDLOG*/
 346+
 347+ $('shade').toggle();
 348+ if(this.pendingElement)
 349+ this.pendingElement.hide();
 350+ this.pendingElement = new OBPendingIndicator($('shade'));
 351+ this.pendingElement.show();
 352+
 353+ var params = "";
 354+ var nodes = $A(document.getElementsByName('nmdel'));
 355+ var dNodes = nodes.select(function(node) {
 356+ return node.checked;
 357+ });
 358+ dNodes.each(function(node) {
 359+ params += node.value + ',';
 360+ });
 361+ sajax_do_call('smwf_nm_NotifyAccess', ["delNotify", params], this.delDone.bind(this));
 362+},
 363+/**
 364+* @param request Request of AJAX call
 365+*/
 366+delDone:function(request){
 367+ this.pendingElement.hide();
 368+ var nodes = $A(document.getElementsByName('nmdel'));
 369+ var sid='';
 370+ var idx=1;
 371+ nodes.each(function(node) {
 372+ if(node.checked) sid += idx+',';
 373+ idx++;
 374+ return node.checked;
 375+ });
 376+ var ids = sid.split(',');
 377+ for(i=ids.length-2;i>=0;--i){
 378+ $('nmtable').deleteRow(ids[i]);
 379+ }
 380+ alert(request.responseText);
 381+ $('shade').toggle();
 382+},
 383+
 384+delall:function(chked){
 385+ var nodes = $A(document.getElementsByName('nmdel'));
 386+ nodes.each(function(node) {
 387+ node.checked = chked;
 388+ });
 389+},
 390+enableall:function(chked){
 391+ var nodes = $A(document.getElementsByName('nmenable'));
 392+ nodes.each(function(node) {
 393+ node.checked = chked;
 394+ });
 395+},
 396+reportall:function(chked){
 397+ var nodes = $A(document.getElementsByName('nmall'));
 398+ nodes.each(function(node) {
 399+ node.checked = chked;
 400+ });
 401+},
 402+showall:function(chked){
 403+ var nodes = $A(document.getElementsByName('nmsall'));
 404+ nodes.each(function(node) {
 405+ node.checked = chked;
 406+ });
 407+},
 408+resetNotify:function(){
 409+ this.delall(false);
 410+ this.enableall(true);
 411+},
 412+updateDelegate:function(){
 413+ /*STARTLOG*/
 414+ if(window.smwhgLogger){
 415+ smwhgLogger.log("Update delegates","NM","update_delegates");
 416+ }
 417+ /*ENDLOG*/
 418+
 419+ $('shade').toggle();
 420+ if(this.pendingElement)
 421+ this.pendingElement.hide();
 422+ this.pendingElement = new OBPendingIndicator($('shade'));
 423+ this.pendingElement.show();
 424+
 425+ var params = "";
 426+ var nodes = $A(document.getElementsByName('nmdelegate'));
 427+ var dNodes = nodes.select(function(node) {
 428+ return (node.value != '');
 429+ });
 430+ dNodes.each(function(node) {
 431+ params += node.id.substring(4) + ':' + node.value + '|';
 432+ });
 433+ sajax_do_call('smwf_nm_NotifyAccess', ["updateDelegates", params], this.updateDone.bind(this));
 434+},
 435+copyToClipboard:function(id){
 436+ /*STARTLOG*/
 437+ if(window.smwhgLogger){
 438+ smwhgLogger.log("Copy nm rss to clipboard","NM","rss_copied");
 439+ }
 440+ /*ENDLOG*/
 441+ var text = $(id).value;
 442+ var succ = 'The RSS feed url was successfully copied to your clipboard';
 443+ var fail = 'Your browser does not allow clipboard access.\nThe RSS feed url could not be copied to your clipboard.\nPlease copy the RSS feed url manually.';
 444+ if (window.clipboardData){ //IE
 445+ window.clipboardData.setData("Text", text);
 446+ alert(succ);
 447+ }
 448+ else if (window.netscape) {
 449+ try {
 450+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 451+ var clip = Components.classes['@mozilla.org/widget/clipboard;1'].createInstance(Components.interfaces.nsIClipboard);
 452+ if (!clip){
 453+ alert(fail);
 454+ return;
 455+ }
 456+ var trans = Components.classes['@mozilla.org/widget/transferable;1'].createInstance(Components.interfaces.nsITransferable);
 457+ if (!trans){
 458+ alert(fail);
 459+ return;
 460+ }
 461+ trans.addDataFlavor('text/unicode');
 462+ var str = new Object();
 463+ var len = new Object();
 464+ var str = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString);
 465+ str.data=text;
 466+ trans.setTransferData("text/unicode",str,text.length*2);
 467+ var clipid=Components.interfaces.nsIClipboard;
 468+ if (!clip){
 469+ alert(fail);
 470+ return;
 471+ }
 472+ clip.setData(trans,null,clipid.kGlobalClipboard);
 473+ alert(succ);
 474+ }
 475+ catch (e) {
 476+ alert(fail);
 477+ }
 478+ }
 479+ else{
 480+ alert(fail);
 481+ }
 482+},
 483+
 484+// this function is highly depend on SpecialPage : QueryInterface
 485+saveQueryToNotify:function() {
 486+ if(!$('addnotifydialogue')) {
 487+ var askdlg = $('showAsk');
 488+ var nmdlg = document.createElement("div");
 489+ askdlg.parentNode.insertBefore(nmdlg, askdlg);
 490+ nmdlg.id = "addnotifydialogue";
 491+ nmdlg.className = "topDialogue";
 492+ nmdlg.style.display = "none";
 493+ var html = "Do you really want to receive any notification of article-updates followed after your query?<br/>";
 494+ html += "Name of the notification: <input type=\"text\" id=\"nmqname\"/><br />";
 495+ html += "<input type=\"checkbox\" id=\"nmqrall\" checked>&nbsp;Notify me all semantic attributes\' change of monitored pages (Report All)<br />";
 496+ html += "<input type=\"checkbox\" id=\"nmqsall\">&nbsp;Show all query results with notification (Show All)<br />";
 497+ html += "<span class=\"qibutton\" onclick=\"notifyhelper.doSaveToNotifyQI()\">OK</span>&nbsp;<span class=\"qibutton\" onclick=\"notifyhelper.doCancelNotifyQI()\">Cancel</span>";
 498+ nmdlg.innerHTML = html;
 499+ }
 500+ $('shade').toggle();
 501+ $('addnotifydialogue').toggle();
 502+},
 503+doSaveToNotifyQI:function(){
 504+ if(window.smwhgLogger){
 505+ smwhgLogger.log("Save Notify","QI","save_notify");
 506+ }
 507+ if(this.pendingElement)
 508+ this.pendingElement.hide();
 509+ this.pendingElement = new OBPendingIndicator($('shade'));
 510+ this.pendingElement.show();
 511+
 512+ if($('nmqname').value=="") {
 513+ alert(nmLanguage.getMessage('NM_EMPTY_NOTIFYNAME'));
 514+ }
 515+ else if (!qihelper.queries[0].isEmpty()){ //only do this if the query is not empty
 516+ var ask = qihelper.recurseQuery(0, "parser"); // Get full ask syntax
 517+ qihelper.queries[0].getDisplayStatements().each(function(s) { ask += "\n| ?" + s});
 518+ if($('layout_intro').value!="") ask += "\n| intro=" + $('layout_intro').value;
 519+ if($('layout_sort').value!=gLanguage.getMessage('QI_ARTICLE_TITLE')) ask += "\n| sort=" + $('layout_sort').value;
 520+ if($('layout_limit').value!="") ask += "\n| limit=" + $('layout_limit').value;
 521+ if($('layout_label').value!="") ask += "\n| mainlabel=" + $('layout_label').value;
 522+ if($('layout_order').value!="ascending") ask += "\n| order=descending";
 523+ if($('layout_default').value!="") ask += "\n| default=" + $('layout_default').value;
 524+ if(!$('layout_headers').checked) ask += "\n| headers=hide";
 525+
 526+ var params = ask.replace(/&/gm, "&amp;").replace(/,/gm, "&comma;") + ",";
 527+ params += ($('nmqrall').checked ? 1 : 0) + ",";
 528+ params += ($('nmqsall').checked ? 1 : 0) + ",";
 529+ params += $('nmqname').value.replace(/&/gm, "&amp;").replace(/,/gm, "&comma;") + ",";
 530+ params += ($('nmd_new') ? $('nmd_new').value : "");
 531+ sajax_do_call('smwf_nm_NotifyAccess', ["addNotify", params], this.saveNotifyQI.bind(this));
 532+ }
 533+ else { // query is empty
 534+ alert(gLanguage.getMessage('QI_EMPTY_QUERY'));
 535+ }
 536+},
 537+saveNotifyQI:function(request){
 538+ this.pendingElement.hide();
 539+ var s = request.responseText;
 540+ if(s.substring(0,1)=="1") {
 541+ var enabled = (s.substring(1,2)=='1');
 542+ s = s.substring(2);
 543+ var i = s.indexOf(",");
 544+ var nid = s.substring(0,i);
 545+ alert("Notify create successfully!" + s.substring(i+1));
 546+ } else alert(s);
 547+ $('addnotifydialogue').toggle();
 548+ $('shade').toggle();
 549+},
 550+doCancelNotifyQI:function(){
 551+ $('addnotifydialogue').toggle();
 552+ $('shade').toggle();
 553+}
 554+
 555+} //end class notifyHelper
 556+
 557+Event.observe(window,'load',initialize_notify);
 558+
 559+function initialize_notify(){
 560+ notifyhelper = new NotifyHelper();
 561+
 562+ // SMW / Halo extension contains wibbit which hooks all checkboxes,
 563+ // have to load the event handler afterwards
 564+ if($('nmemail')) $('nmemail').onclick = function() {notifyhelper.doUpdateMail();};
 565+}
Index: trunk/extensions/SemanticNotifyMe/skins/nm.css
@@ -0,0 +1,230 @@
 2+#qicontent{
 3+cursor: default;
 4+font-family: sans-serif;
 5+font-size-adjust:none;
 6+font-style:normal;
 7+font-variant:normal;
 8+max-width:100%;
 9+width:100%;
 10+height:550px;
 11+}
 12+
 13+#shade{
 14+position: absolute;
 15+top: 0px;
 16+left: 0px;
 17+background:#444444;
 18+-moz-opacity: 0.2;
 19+filter:alpha(opacity=20);
 20+opacity: 0.2;
 21+-khtml-opacity: 0.2;
 22+width:100%;
 23+height:100%;
 24+z-index: 50;
 25+}
 26+
 27+.nmmenubar{
 28+border: 1px solid #C0C0C0;
 29+background: #DDDDDD;
 30+width:100%;
 31+margin-top:10px;
 32+margin-bottom:10px;
 33+}
 34+
 35+.nmbutton{
 36+font-family: sans-serif;
 37+text-decoration: underline;
 38+color: #0000FF;
 39+cursor: pointer;
 40+padding-right: 5px;
 41+padding-left: 5px;
 42+}
 43+
 44+.dragbox{
 45+float:left;
 46+width:73%;
 47+max-width:73%;
 48+}
 49+
 50+.boxcontent {
 51+border: 1px solid #AAAAAA;
 52+min-width: 70%;
 53+width:100%;
 54+height: 209px;
 55+float:left;
 56+overflow:auto;
 57+}
 58+
 59+.boxcontent img{
 60+cursor:pointer;
 61+}
 62+
 63+.helpcontent {
 64+border: 1px solid #AAAAAA;
 65+min-width: 28%;
 66+width:28%;
 67+height: 209px;
 68+float:left;
 69+overflow:auto;
 70+margin-left:5px;
 71+background-color: #FFFFCC;
 72+border: 1px solid #E6E600;
 73+}
 74+
 75+#querylayout{
 76+width:100%;
 77+border: 1px solid #AAAAAA;
 78+float:left;
 79+margin-top: 10px;
 80+}
 81+
 82+#layouttitle{
 83+background: #DDDDDD;
 84+cursor:pointer;
 85+font-weight: bold;
 86+margin-top:10px;
 87+}
 88+
 89+#layoutcontent{
 90+border-top:1px solid #AAAAAA;
 91+background: #FFFFFF;
 92+}
 93+
 94+#querytitle{
 95+background: #DDDDDD;
 96+cursor:pointer;
 97+font-weight: bold;
 98+}
 99+
 100+#querycontent{
 101+border-top:1px solid #AAAAAA;
 102+background: #FFFFFF;
 103+}
 104+
 105+#querycontent select{
 106+/*width:95%;*/
 107+}
 108+
 109+#previewlayout{
 110+width:100%;
 111+border: 1px solid #AAAAAA;
 112+float:left;
 113+margin-top: 10px;
 114+}
 115+
 116+#previewtitle{
 117+background: #DDDDDD;
 118+cursor:pointer;
 119+font-weight: bold;
 120+}
 121+
 122+#previewcontent{
 123+border-top:1px solid #AAAAAA;
 124+background: #FFFFFF;
 125+}
 126+
 127+
 128+.selectbox{
 129+float:left;
 130+border: 1px solid #AAAAAA;
 131+margin-left:10px;
 132+}
 133+.selectboxheader{
 134+font-weight: bold;
 135+color: #000000;
 136+background: #DDDDDD;
 137+border-bottom: 1px solid #AAAAAA;
 138+}
 139+
 140+.selectboxcontent{
 141+background:#FFFFFF;
 142+padding:5px;
 143+}
 144+
 145+a.minusplus{
 146+ background-image:url(images/minus-pas.gif);
 147+ float:left;
 148+ height:12px;
 149+ margin-right:4px;
 150+ margin-top:3px;
 151+ overflow:hidden;
 152+ width:12px;
 153+}
 154+a.minusplus:hover{
 155+ background-image:url(images/minus-act.gif);
 156+}
 157+
 158+a.plusminus{
 159+ background-image:url(images/plus-pas.gif);
 160+ float:left;
 161+ height:12px;
 162+ margin-right:4px;
 163+ margin-top:3px;
 164+ overflow:hidden;
 165+ width:12px;
 166+}
 167+a.plusminus:hover{
 168+ background-image:url(images/plus-act.gif);
 169+}
 170+
 171+#fullpreviewbox{
 172+position:absolute;
 173+z-index:51;
 174+top:10%;
 175+left:10%;
 176+background:#FFF;
 177+border: 1px solid #AAAAAA;
 178+padding:10px;
 179+}
 180+
 181+#fullpreview{
 182+padding:5px;
 183+max-height:200px;
 184+overflow:auto;
 185+}
 186+
 187+.subquerycell{
 188+border-left:1px solid;
 189+border-color: #AAAAAA;
 190+}
 191+
 192+.topDialogue{
 193+position:absolute;
 194+z-index:51;
 195+top:25%;
 196+left:30%;
 197+background:#FFF;
 198+border: 1px solid #AAAAAA;
 199+padding:5px;
 200+}
 201+
 202+#dialoguecontent tr td{
 203+text-align:right;
 204+}
 205+
 206+#dialoguecontent tr td td{
 207+text-align:left;
 208+}
 209+
 210+button.btn{
 211+color:#000;
 212+font-family:'trebuchet ms',helvetica,sans-serif;
 213+font-size:84%;
 214+font-weight:bold;
 215+background-color:#eee;
 216+border:1px solid;
 217+border-top-color:#c0c0c0;
 218+border-left-color:#c0c0c0;
 219+border-right-color:#606060;
 220+border-bottom-color:#606060;
 221+margin:2px;
 222+cursor: pointer;
 223+}
 224+button.btnhov{
 225+border-top-color:#00f;
 226+border-left-color:#00f;
 227+border-right-color:#00f;
 228+border-bottom-color:#00f;
 229+cursor: pointer;
 230+}
 231+
Index: trunk/extensions/SemanticNotifyMe/skins/images/minus-pas.gif
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/SemanticNotifyMe/skins/images/minus-pas.gif
___________________________________________________________________
Name: svn:mime-type
1232 + application/octet-stream
Index: trunk/extensions/SemanticNotifyMe/skins/images/minus-act.gif
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/SemanticNotifyMe/skins/images/minus-act.gif
___________________________________________________________________
Name: svn:mime-type
2233 + application/octet-stream
Index: trunk/extensions/SemanticNotifyMe/skins/images/delete.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/SemanticNotifyMe/skins/images/delete.png
___________________________________________________________________
Name: svn:mime-type
3234 + application/octet-stream
Index: trunk/extensions/SemanticNotifyMe/skins/images/plus-pas.gif
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/SemanticNotifyMe/skins/images/plus-pas.gif
___________________________________________________________________
Name: svn:mime-type
4235 + application/octet-stream
Index: trunk/extensions/SemanticNotifyMe/skins/images/plus-act.gif
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/SemanticNotifyMe/skins/images/plus-act.gif
___________________________________________________________________
Name: svn:mime-type
5236 + application/octet-stream
Index: trunk/extensions/SemanticNotifyMe/COPYING
@@ -0,0 +1,348 @@
 2+The license text below "----" applies to all files within this distribution, other
 3+than those that are in a directory which contains files named "LICENSE" or
 4+"COPYING", or a subdirectory thereof. For those files, the license text contained in
 5+said file overrides any license information contained in directories of smaller depth.
 6+Alternative licenses are typically used for software that is provided by external
 7+parties, and merely packaged with the Semantic MediaWiki release for convenience.
 8+----
 9+
 10+ GNU GENERAL PUBLIC LICENSE
 11+ Version 2, June 1991
 12+
 13+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
 14+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 15+ Everyone is permitted to copy and distribute verbatim copies
 16+ of this license document, but changing it is not allowed.
 17+
 18+ Preamble
 19+
 20+ The licenses for most software are designed to take away your
 21+freedom to share and change it. By contrast, the GNU General Public
 22+License is intended to guarantee your freedom to share and change free
 23+software--to make sure the software is free for all its users. This
 24+General Public License applies to most of the Free Software
 25+Foundation's software and to any other program whose authors commit to
 26+using it. (Some other Free Software Foundation software is covered by
 27+the GNU Library General Public License instead.) You can apply it to
 28+your programs, too.
 29+
 30+ When we speak of free software, we are referring to freedom, not
 31+price. Our General Public Licenses are designed to make sure that you
 32+have the freedom to distribute copies of free software (and charge for
 33+this service if you wish), that you receive source code or can get it
 34+if you want it, that you can change the software or use pieces of it
 35+in new free programs; and that you know you can do these things.
 36+
 37+ To protect your rights, we need to make restrictions that forbid
 38+anyone to deny you these rights or to ask you to surrender the rights.
 39+These restrictions translate to certain responsibilities for you if you
 40+distribute copies of the software, or if you modify it.
 41+
 42+ For example, if you distribute copies of such a program, whether
 43+gratis or for a fee, you must give the recipients all the rights that
 44+you have. You must make sure that they, too, receive or can get the
 45+source code. And you must show them these terms so they know their
 46+rights.
 47+
 48+ We protect your rights with two steps: (1) copyright the software, and
 49+(2) offer you this license which gives you legal permission to copy,
 50+distribute and/or modify the software.
 51+
 52+ Also, for each author's protection and ours, we want to make certain
 53+that everyone understands that there is no warranty for this free
 54+software. If the software is modified by someone else and passed on, we
 55+want its recipients to know that what they have is not the original, so
 56+that any problems introduced by others will not reflect on the original
 57+authors' reputations.
 58+
 59+ Finally, any free program is threatened constantly by software
 60+patents. We wish to avoid the danger that redistributors of a free
 61+program will individually obtain patent licenses, in effect making the
 62+program proprietary. To prevent this, we have made it clear that any
 63+patent must be licensed for everyone's free use or not licensed at all.
 64+
 65+ The precise terms and conditions for copying, distribution and
 66+modification follow.
 67+
 68+ GNU GENERAL PUBLIC LICENSE
 69+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 70+
 71+ 0. This License applies to any program or other work which contains
 72+a notice placed by the copyright holder saying it may be distributed
 73+under the terms of this General Public License. The "Program", below,
 74+refers to any such program or work, and a "work based on the Program"
 75+means either the Program or any derivative work under copyright law:
 76+that is to say, a work containing the Program or a portion of it,
 77+either verbatim or with modifications and/or translated into another
 78+language. (Hereinafter, translation is included without limitation in
 79+the term "modification".) Each licensee is addressed as "you".
 80+
 81+Activities other than copying, distribution and modification are not
 82+covered by this License; they are outside its scope. The act of
 83+running the Program is not restricted, and the output from the Program
 84+is covered only if its contents constitute a work based on the
 85+Program (independent of having been made by running the Program).
 86+Whether that is true depends on what the Program does.
 87+
 88+ 1. You may copy and distribute verbatim copies of the Program's
 89+source code as you receive it, in any medium, provided that you
 90+conspicuously and appropriately publish on each copy an appropriate
 91+copyright notice and disclaimer of warranty; keep intact all the
 92+notices that refer to this License and to the absence of any warranty;
 93+and give any other recipients of the Program a copy of this License
 94+along with the Program.
 95+
 96+You may charge a fee for the physical act of transferring a copy, and
 97+you may at your option offer warranty protection in exchange for a fee.
 98+
 99+ 2. You may modify your copy or copies of the Program or any portion
 100+of it, thus forming a work based on the Program, and copy and
 101+distribute such modifications or work under the terms of Section 1
 102+above, provided that you also meet all of these conditions:
 103+
 104+ a) You must cause the modified files to carry prominent notices
 105+ stating that you changed the files and the date of any change.
 106+
 107+ b) You must cause any work that you distribute or publish, that in
 108+ whole or in part contains or is derived from the Program or any
 109+ part thereof, to be licensed as a whole at no charge to all third
 110+ parties under the terms of this License.
 111+
 112+ c) If the modified program normally reads commands interactively
 113+ when run, you must cause it, when started running for such
 114+ interactive use in the most ordinary way, to print or display an
 115+ announcement including an appropriate copyright notice and a
 116+ notice that there is no warranty (or else, saying that you provide
 117+ a warranty) and that users may redistribute the program under
 118+ these conditions, and telling the user how to view a copy of this
 119+ License. (Exception: if the Program itself is interactive but
 120+ does not normally print such an announcement, your work based on
 121+ the Program is not required to print an announcement.)
 122+
 123+These requirements apply to the modified work as a whole. If
 124+identifiable sections of that work are not derived from the Program,
 125+and can be reasonably considered independent and separate works in
 126+themselves, then this License, and its terms, do not apply to those
 127+sections when you distribute them as separate works. But when you
 128+distribute the same sections as part of a whole which is a work based
 129+on the Program, the distribution of the whole must be on the terms of
 130+this License, whose permissions for other licensees extend to the
 131+entire whole, and thus to each and every part regardless of who wrote it.
 132+
 133+Thus, it is not the intent of this section to claim rights or contest
 134+your rights to work written entirely by you; rather, the intent is to
 135+exercise the right to control the distribution of derivative or
 136+collective works based on the Program.
 137+
 138+In addition, mere aggregation of another work not based on the Program
 139+with the Program (or with a work based on the Program) on a volume of
 140+a storage or distribution medium does not bring the other work under
 141+the scope of this License.
 142+
 143+ 3. You may copy and distribute the Program (or a work based on it,
 144+under Section 2) in object code or executable form under the terms of
 145+Sections 1 and 2 above provided that you also do one of the following:
 146+
 147+ a) Accompany it with the complete corresponding machine-readable
 148+ source code, which must be distributed under the terms of Sections
 149+ 1 and 2 above on a medium customarily used for software interchange; or,
 150+
 151+ b) Accompany it with a written offer, valid for at least three
 152+ years, to give any third party, for a charge no more than your
 153+ cost of physically performing source distribution, a complete
 154+ machine-readable copy of the corresponding source code, to be
 155+ distributed under the terms of Sections 1 and 2 above on a medium
 156+ customarily used for software interchange; or,
 157+
 158+ c) Accompany it with the information you received as to the offer
 159+ to distribute corresponding source code. (This alternative is
 160+ allowed only for noncommercial distribution and only if you
 161+ received the program in object code or executable form with such
 162+ an offer, in accord with Subsection b above.)
 163+
 164+The source code for a work means the preferred form of the work for
 165+making modifications to it. For an executable work, complete source
 166+code means all the source code for all modules it contains, plus any
 167+associated interface definition files, plus the scripts used to
 168+control compilation and installation of the executable. However, as a
 169+special exception, the source code distributed need not include
 170+anything that is normally distributed (in either source or binary
 171+form) with the major components (compiler, kernel, and so on) of the
 172+operating system on which the executable runs, unless that component
 173+itself accompanies the executable.
 174+
 175+If distribution of executable or object code is made by offering
 176+access to copy from a designated place, then offering equivalent
 177+access to copy the source code from the same place counts as
 178+distribution of the source code, even though third parties are not
 179+compelled to copy the source along with the object code.
 180+
 181+ 4. You may not copy, modify, sublicense, or distribute the Program
 182+except as expressly provided under this License. Any attempt
 183+otherwise to copy, modify, sublicense or distribute the Program is
 184+void, and will automatically terminate your rights under this License.
 185+However, parties who have received copies, or rights, from you under
 186+this License will not have their licenses terminated so long as such
 187+parties remain in full compliance.
 188+
 189+ 5. You are not required to accept this License, since you have not
 190+signed it. However, nothing else grants you permission to modify or
 191+distribute the Program or its derivative works. These actions are
 192+prohibited by law if you do not accept this License. Therefore, by
 193+modifying or distributing the Program (or any work based on the
 194+Program), you indicate your acceptance of this License to do so, and
 195+all its terms and conditions for copying, distributing or modifying
 196+the Program or works based on it.
 197+
 198+ 6. Each time you redistribute the Program (or any work based on the
 199+Program), the recipient automatically receives a license from the
 200+original licensor to copy, distribute or modify the Program subject to
 201+these terms and conditions. You may not impose any further
 202+restrictions on the recipients' exercise of the rights granted herein.
 203+You are not responsible for enforcing compliance by third parties to
 204+this License.
 205+
 206+ 7. If, as a consequence of a court judgment or allegation of patent
 207+infringement or for any other reason (not limited to patent issues),
 208+conditions are imposed on you (whether by court order, agreement or
 209+otherwise) that contradict the conditions of this License, they do not
 210+excuse you from the conditions of this License. If you cannot
 211+distribute so as to satisfy simultaneously your obligations under this
 212+License and any other pertinent obligations, then as a consequence you
 213+may not distribute the Program at all. For example, if a patent
 214+license would not permit royalty-free redistribution of the Program by
 215+all those who receive copies directly or indirectly through you, then
 216+the only way you could satisfy both it and this License would be to
 217+refrain entirely from distribution of the Program.
 218+
 219+If any portion of this section is held invalid or unenforceable under
 220+any particular circumstance, the balance of the section is intended to
 221+apply and the section as a whole is intended to apply in other
 222+circumstances.
 223+
 224+It is not the purpose of this section to induce you to infringe any
 225+patents or other property right claims or to contest validity of any
 226+such claims; this section has the sole purpose of protecting the
 227+integrity of the free software distribution system, which is
 228+implemented by public license practices. Many people have made
 229+generous contributions to the wide range of software distributed
 230+through that system in reliance on consistent application of that
 231+system; it is up to the author/donor to decide if he or she is willing
 232+to distribute software through any other system and a licensee cannot
 233+impose that choice.
 234+
 235+This section is intended to make thoroughly clear what is believed to
 236+be a consequence of the rest of this License.
 237+
 238+ 8. If the distribution and/or use of the Program is restricted in
 239+certain countries either by patents or by copyrighted interfaces, the
 240+original copyright holder who places the Program under this License
 241+may add an explicit geographical distribution limitation excluding
 242+those countries, so that distribution is permitted only in or among
 243+countries not thus excluded. In such case, this License incorporates
 244+the limitation as if written in the body of this License.
 245+
 246+ 9. The Free Software Foundation may publish revised and/or new versions
 247+of the General Public License from time to time. Such new versions will
 248+be similar in spirit to the present version, but may differ in detail to
 249+address new problems or concerns.
 250+
 251+Each version is given a distinguishing version number. If the Program
 252+specifies a version number of this License which applies to it and "any
 253+later version", you have the option of following the terms and conditions
 254+either of that version or of any later version published by the Free
 255+Software Foundation. If the Program does not specify a version number of
 256+this License, you may choose any version ever published by the Free Software
 257+Foundation.
 258+
 259+ 10. If you wish to incorporate parts of the Program into other free
 260+programs whose distribution conditions are different, write to the author
 261+to ask for permission. For software which is copyrighted by the Free
 262+Software Foundation, write to the Free Software Foundation; we sometimes
 263+make exceptions for this. Our decision will be guided by the two goals
 264+of preserving the free status of all derivatives of our free software and
 265+of promoting the sharing and reuse of software generally.
 266+
 267+ NO WARRANTY
 268+
 269+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
 270+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
 271+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
 272+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
 273+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 274+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
 275+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
 276+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
 277+REPAIR OR CORRECTION.
 278+
 279+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 280+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
 281+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
 282+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
 283+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
 284+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
 285+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
 286+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
 287+POSSIBILITY OF SUCH DAMAGES.
 288+
 289+ END OF TERMS AND CONDITIONS
 290+
 291+ How to Apply These Terms to Your New Programs
 292+
 293+ If you develop a new program, and you want it to be of the greatest
 294+possible use to the public, the best way to achieve this is to make it
 295+free software which everyone can redistribute and change under these terms.
 296+
 297+ To do so, attach the following notices to the program. It is safest
 298+to attach them to the start of each source file to most effectively
 299+convey the exclusion of warranty; and each file should have at least
 300+the "copyright" line and a pointer to where the full notice is found.
 301+
 302+ <one line to give the program's name and a brief idea of what it does.>
 303+ Copyright (C) <year> <name of author>
 304+
 305+ This program is free software; you can redistribute it and/or modify
 306+ it under the terms of the GNU General Public License as published by
 307+ the Free Software Foundation; either version 2 of the License, or
 308+ (at your option) any later version.
 309+
 310+ This program is distributed in the hope that it will be useful,
 311+ but WITHOUT ANY WARRANTY; without even the implied warranty of
 312+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 313+ GNU General Public License for more details.
 314+
 315+ You should have received a copy of the GNU General Public License
 316+ along with this program; if not, write to the Free Software
 317+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 318+
 319+
 320+Also add information on how to contact you by electronic and paper mail.
 321+
 322+If the program is interactive, make it output a short notice like this
 323+when it starts in an interactive mode:
 324+
 325+ Gnomovision version 69, Copyright (C) year name of author
 326+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
 327+ This is free software, and you are welcome to redistribute it
 328+ under certain conditions; type `show c' for details.
 329+
 330+The hypothetical commands `show w' and `show c' should show the appropriate
 331+parts of the General Public License. Of course, the commands you use may
 332+be called something other than `show w' and `show c'; they could even be
 333+mouse-clicks or menu items--whatever suits your program.
 334+
 335+You should also get your employer (if you work as a programmer) or your
 336+school, if any, to sign a "copyright disclaimer" for the program, if
 337+necessary. Here is a sample; alter the names:
 338+
 339+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
 340+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
 341+
 342+ <signature of Ty Coon>, 1 April 1989
 343+ Ty Coon, President of Vice
 344+
 345+This General Public License does not permit incorporating your program into
 346+proprietary programs. If your program is a subroutine library, you may
 347+consider it more useful to permit linking proprietary applications with the
 348+library. If this is what you want to do, use the GNU Library General
 349+Public License instead of this License.
Index: trunk/extensions/SemanticNotifyMe/README
@@ -0,0 +1,29 @@
 2+== About ==
 3+
 4+Semantic NotifyMe is a project for extending Semantic MediaWiki extension with
 5+"realtime notification" functions that allows users to get realtime notifications
 6+on semantic property-value changes. For details and further links, see
 7+http://www.mediawiki.org/wiki/Extension:Semantic_NotifyMe.
 8+
 9+Notes on installing Semantic NotifyMe are found in the file INSTALL.
 10+
 11+== Contact ==
 12+
 13+If you have remarks, questions, or suggestions, please send them to
 14+ning@teammersion.com.
 15+
 16+Bugs should be filed at MediaZilla, http://bugzilla.wikimedia.org/.
 17+
 18+== Developers ==
 19+
 20+Development is coordinated by Ning Hu and Justin Zhang.
 21+
 22+Semantic NotifyMe development is funded by Vulcan Inc.
 23+Semantic NotifyMe has also been supported in the Halo
 24+project funded by Vulcan Inc., where development is coordinated
 25+by ontoprise GmbH, Karlsruhe, Germany.
 26+
 27+== Contributors ==
 28+
 29+N/A
 30+

Comments

#Comment by Siebrand (talk | contribs)   06:46, 13 January 2010

Please set svn:eol-style native. If you haven't set svn auto props, please also check Subversion/auto-props. Thanks.

#Comment by Ning~mediawikiwiki (talk | contribs)   09:03, 13 January 2010

Done. Thanks.

#Comment by Siebrand (talk | contribs)   06:55, 13 January 2010
  • lots of hard coded English text. Please mind your MediaWiki i18n -> Localisation is a must read.
  • indentation is inconsistent. Both spaces and tabs are used -> coding style.

Please fix. Thanks.

#Comment by MarkAHershberger (talk | contribs)   17:19, 14 July 2010

Looks like all these have been fixed. Could you review and remove the fixme tag if ok?

#Comment by Siebrand (talk | contribs)   18:07, 15 July 2010

Setting to new for now; on holdiday for the next few weeks and wouldn't want this to hold up further review.

Status & tagging log