709 Tage zuvor: Twitter-Home als Feed mit Javascript

Vor gut zwei Jahren hat Twitter einen wichtigen Schritt zur Datensicherheit gemacht. Mit EinfĂŒhrung ihrer API 1.1 wurde zum AusfĂŒhren vieler Funktionen eine OAuth Authentifizierung nötig. Und bei der Implementierung in (Web-) Applikationen ist dies auch meistens kein Problem. Anders sieht es da schon mit JavaScript aus.

Generell ist von einer Implementierung von OAuth in JavaScript auch deutlich abzuraten! Da die privaten SchlĂŒssel zur einer eindeutigen Identifizierung im Klartext dem Client vorliegen mĂŒssen, gibt es keine Möglichkeit diese ausreichend vor Fremdzugriff zu schĂŒtzen. FĂŒr mein letzte Projekt, eine personalisierte Browser-Startseite, war jedoch eine Server-seitige Implementation nicht zielfĂŒhrend.

kbo Startpage

Beim Herausarbeiten der FunktionalitĂ€t stieß ich immer wieder auf Aussagen das dies generell nicht möglich sei (sogar von Twitter-Mitarbeitern). Selbstredend ist dies nicht korrekt; wenn auch die Implementation, vor allem dank des faszinierenden Signatur-Prozesses, durchaus herausfordernd ist. Um meinen Teil zum aufrĂ€umen mit diesem GerĂŒcht beizutragen, hier jedenfalls die FrĂŒchte meiner Arbeit.

Um das Skript auszufĂŒhren benötigt man:

  1. Eine Twitter-App (zu erstellen)
  2. Authentifizierten Zugriff der App auf das eigene Profil
  3. Die CryptoJS library (hmac-sha1.js reicht)
  4. jQuery (zumindest in dem Beispiel)

FĂŒllt man die Variablen fĂŒr Consumer und Access Token aus, sollte der Aufruf von loadTwitter() das Array tweets[] mit den letzten Tweets der Twitter Home-Timeline befĂŒllen. Und die kann man dann, zum Beispiel, in einer personalisierten Startseite verwenden.

/*
 * Retrieve Tweets from your personal Twitter home via REST.
 * (c)2015 by Kai Boenke [code@boenke.info]
 * Requires CryptoJS (hmac-sha1.js): https://code.google.com/p/crypto-js/
 * Requires jQUery: https://jquery.com/
 */
var tweets = [];
function loadTwitter(){
	twitterURL =			"https://api.twitter.com/1.1/statuses/home_timeline.json";
	twitterURLmethod =		"GET",
	twitterKey =			"xxxxxxxxxxxxxxxxxxxxxxxxx"; //Consumer Key (User)
	twitterSecret =			"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; //Consumer Secret (User)
	twitterToken =			"99999999-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; //Access Token (App)
	twitterTokenSecret =	"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; //Access Token Secret (App)
	twitterNonce =			getNonce(32);
	twitterTimestamp =		Math.round(Date.now()/1000);
	
	// Generate oAuth Signature (https://dev.twitter.com/oauth/overview/creating-signatures)
	twitterParameter = [
		encodeURI('oauth_consumer_key='+twitterKey),
		encodeURI('oauth_nonce='+twitterNonce),
		encodeURI('oauth_signature_method='+"HMAC-SHA1"),
		encodeURI('oauth_timestamp='+twitterTimestamp),
		encodeURI('oauth_token='+twitterToken),
		encodeURI('oauth_version='+"1.0")
	];
	twitterParameter.sort();
	twitterSignatureBase =	twitterURLmethod +"&"+ encodeURIComponent(twitterURL) +"&"+ encodeURIComponent(twitterParameter.join("&"));
	twitterSignatureKey =	encodeURIComponent(twitterSecret) +"&"+ encodeURIComponent(twitterTokenSecret);
	twitterSignature =		encodeURIComponent(CryptoJS.HmacSHA1(twitterSignatureBase, twitterSignatureKey).toString(CryptoJS.enc.Base64));
	
	// Get tweets
	$.ajax({
		type:		twitterURLmethod,
		url:		twitterURL,
		headers:	{
			'Authorization': 'OAuth '+
				'oauth_consumer_key="'+ twitterKey +'", '+
				'oauth_nonce="'+ twitterNonce +'", '+
				'oauth_signature="'+ twitterSignature +'", '+
				'oauth_signature_method="HMAC-SHA1", '+
				'oauth_timestamp="'+ twitterTimestamp +'", '+
				'oauth_token="'+ twitterToken +'", '+
				'oauth_version="1.0"'
		}
	}).done(function(twitterData){
		// Retrieve tweets
		$.each(twitterData, function(i, tweet){
			tweets.push({
				timestamp: (new Date(tweet.created_at)).getTime(),
				link: "https://twitter.com/"+ tweet.user.screen_name +"/status/"+ tweet.id_str,
				value: tweet.text
			});
		});
	});
}

904 Tage zuvor: Intelligenz fĂŒr das Intelligente Wohnen

So ein Smart-Home ist schon etwas feines: Lichter gehen von alleine aus, wenn die werten Familienmitglieder es mal wieder vergessen, der freundliche Hausgeist erinnert einen daran rechtzeitig die MĂŒlltonnen an die Strasse zu stellen und wenn mal niemand zu Hause ist kann das Haus von ganz allein so tun als ob es doch so wĂ€re. Um das zu bewerkstelligen muss man zum einen selbstredend die entsprechende Hardware haben. Zum anderen braucht man auch noch eine Zentrale, die alles miteinander verbindet – und letztendlich steuert.

Soweit, so einfach. Doch es bringt auch eine Reihe neuer Herausforderungen mit sich. Zum Beispiel die neuartige Frage warum eine Lampe sich morgens partout nicht einschalten lassen will. Oder weshalb das Wohnzimmer auf einmal rot leuchtet. Oder warum morgens das Radio lĂ€uft. Oder warum allabendlich die Beleuchtung im Garten angeht. Oder, oder, oder…

Was dafĂŒr bislang fehlte war ein Schaltplan fĂŒr das Smarthome; denn zumindest meine Schaltzentrale bietet keine adĂ€quate Lösung um derlei ZusammenhĂ€nge klar darzustellen. Aber dafĂŒr gibt es eine API samt XML Export der Konfiguration. Was fehlt ist lediglich ein Konverter in eine anschaulichere Darstellungsweise, zum Beispiel ein UML Diagramm (mein persönlicher Favorit fĂŒr derlei ist yUML). Und das sieht dann in etwa so aus:

Schaltbild eines Smarthome

Das Bild muss sich in seiner KomplexitÀt nicht unbedingt hinter klassischen SchaltplÀnen verstecken, hilft aber ebenso gut um die Fehlerursache einzugrenzen.

Das Script zum konvertieren der XML Datei in ein PHP-Array gibt es in meinem github Repository. Viel Spaß beim visualisieren!

1462 Tage zuvor: Sticky-Navigation in Prototype

Sticky-Navigationen sind ja irgendwie ein Trend: Die Navigationsleiste einer Webseite bleibt dabei am oberen Fensterrand kleben, wenn man nach unten scrollt. Ist ja auch eine praktische Sache, manchmal jedenfalls.

Die gĂ€ngigen Implementierungen greifen jedoch auf jQuery zurĂŒck. Soweit nichts verwerfliches, ist es doch eine weit verbreitete Standard-Bibliothek. FĂŒr die Implementierung auf diesem Blog wollte ich jedoch die ohnehin bereits verwendete Bibliothek Prototype nutzen. Doch leider gab es dazu keine Lösung. Oder zumindest konnte ich keine finden. FĂŒr die nachfolgenden Leidensgenossen hier ein passende Lösung:


1475 Tage zuvor: Bubble-Sort in PHP

Da bin ich die Tage doch glatt dazu gekommen einige (Er-)Kenntnisse meines Algorithmus-Kurses vom letzten Jahr anzuwenden. Grund war die fehlende Funktion zur Sortierung eines Mehrdimensionalen Arrays basierend auf einem Sub-Key. Aber gut, so konnte ich mir wenigstens beweisen das ich das Konzept noch verstanden habe:

// (Bubble)Sort
do{
	$swapped = false;
	for($i=0; $i<(count($calendar)-1); $i++){
		if($calendar[$i]["stamp"] > $calendar[$i+1]["stamp"]){
			$swap = $calendar[$i];
			$calendar[$i] = $calendar[$i+1];
			$calendar[$i+1] = $swap;
			$swapped = true;
		}
	}
}while($swapped != false);

Mal schauen wann mir dann eine Anwendung fĂŒr die Algorithmen zu Graphen in den Schoss fĂ€llt.

PS: Wieso gibt es in PHP eigentlich kein array_swap()?

1898 Tage zuvor: Drucker-Monitoring mit Munin

Munin ist ein tolles Tool. Aber das sagte ich ja bereits. Vor allem die leichte Erweiterbarkeit mit neuen Plugins macht es mir immer wieder aufs neue sympatisch. Wer es noch nicht kennt (und eine nennenswerte Anzahl an Hosts im Netzwerk hat), sollt es sich definitiv nĂ€her anschauen! Ich nutze es auch beruflich um einige Legacy Systeme und Drucker zu ĂŒberwachen.

Nachdem wir vor einiger Zeit einen neuen Drucker fĂŒr das Heimnetz angeschafft haben, lag es nahe das entsprechende Plugin auch in meiner privaten Munin-Installation einzusetzen. HauptsĂ€chlich um rechtzeitig neuen Toner kaufen zu können.

Und weil ich den Code so schön aufgearbeitet habe, gibt es das Plugin jetzt auch in meinem Google-Code Repository zum Download (und erweitern).

1959 Tage zuvor: Code schreiben, Code lesen

Denkt man an Programmieren fĂ€llt einem zunĂ€chst nur die Logik ein: Der Ablauf, den ein Programm erledigen muss um ein gewĂŒnschtes Resultat zu liefern. Hat man das erreicht stellt sich meist als nĂ€chste Frage die Effizienz: Wie kann ich das Resultat möglichst schnell oder mit möglichst wenig Ressourcen erreichen? Ein eher GrundsĂ€tzlicheres Problem stellt sich schließlich bei der Implementation: Welche Namenskonventionen verwende ich fĂŒr meine Funktionieren, Variablen und Klassenbezeichnungen? Allerdings gibt es auch noch eine weitere, eher unauffĂ€llige Fragestellung: Wie kann ich mein Programm verstĂ€ndlich schreiben? Meistens wird hier auf eine ausgiebige Dokumentation oder sinnvolle Kommentare im Quellcode verwiesen – doch in der RealitĂ€t werden diese eher spĂ€rlich umgesetzt.

Auf dieser Situation aufbauend haben Robert Green und Henry Ledgard ein paar GrundsĂ€tze ĂŒber Namenskonventionen und Layout von Quellcode erstellt und in der ACM Queue veröffentlicht. Ihr Ziel war es Code so zu schreiben, dass er auch ohne Dokumentation einfach und verstĂ€ndlich nachzuvollziehen ist – nicht nur fĂŒr andere, sondern auch fĂŒr einen selbst. Wer sich jemals Jahre spĂ€ter durch den eigenen Spaghetticode wĂŒhlen musste um Modifikationen an einem Programm vorzunehmen, kann das sicherlich nur zu gut nachempfinden. Doch wie funktioniert dieser Ansatz? Ein Beispiel aus ihrer Veröffentlichung

char c1;
c1 = getChoice();
Switch(c1){
  case 'q': case 'Q':  quit();                break;
  case 'e': case 'E':  enterPerson(content);  break;
  case 'd': case 'D':  delPerson(content);    break;
  case 's': case 'S':  sortByName();          break;
  case 'l': case 'L':  showAll();             break;
  case 'f': case 'F':  searchByName(content); break;
  case default:        System.out.println("--Invalid Command!!\n"):
}

Wie zu sehen ist legen sie auf die Gestaltung des Codes wert – Anweisungen werden in logische Blöcke strukturiert so dargestellt, dass man durch einfaches betrachten erkennen unter welcher Bedingung welche Aktion ausgefĂŒhrt wird. In ihren GrundsĂ€tzen gehen sie allerdings auch auf die Benennung (und Lesbarkeit) von Variablen, Funktionen und Klassen ein. Stets mit dem Ziel den Code fĂŒr sich selbst sprechen zu lassen. Ein Artikel den sich, wie ich finde, zu lesen lohnt!

Ich persönlich gerate speziell bei den Namenskonventionen regelmĂ€ĂŸig ins Straucheln – was zu einem guten Teil auch von meiner Trial-and-Error Programmierung herrĂŒhrt: Variablen Ă€ndern durchaus öfters im Laufe der Versionen ihre Nutzung, Funktionen ihren Umfang und Klassen ihre Bestimmung (was dank Refactoring kein großes Problem mehr darstellt). Und eine wirkliche Dokumentation hat bislang noch keines meiner ohnehin eher kleineren Projekte verpasst bekommen. Daher werde ich mich bemĂŒhen bei meinen zukĂŒnftigen Projekten diese GrundsĂ€tze zu beherzigen.

1962 Tage zuvor: Arduino-Senso

Als ich im letzten Jahr endlich den Open Camera Controller fertiggestellt hatte, stellte sich die Frage: Was mache ich jetzt mit meinem Arduino Eine Antwort war schnell gefunden: Einen Senso-Klon) bauen! Leider sich das Projekt dann doch wesentlich lÀnger hingezogen als gedacht. Einen nicht unwesentlichen Teil haben meine eher rostigen Elektronik-Kenntnisse dazu beigetragen. Aber nichts desto trotz ist es nun endlich vollbracht! Auch wenn es sicherlich nicht schönsten Lösung ist, hier eine simplifizierte Version meines Schaltplans.

Zum 1:1 Nachbau möchte ich so allerdings nicht raten – Pin 13 des Arduino eignet sich nicht sonderlich gut als Input (wegen der Standard Notification-LED). Und auch sollte man fĂŒr blaue LEDs einen höheren Widerstand als fĂŒr die anderen LEDs (rot, gelb und grĂŒn in meinem Fall) benutzen – sie sind schlichtweg viel zu hell!

Die Software zur Hardware war wiederum erstaunlich schnell geschrieben – lediglich wenige Stunden (intensives Testen inklusive) hat es gedauert. Ganz in Anlehnung an das Hardware-Layout ist auch der Code nicht sonderlich ansehnlich. Dennoch sei er hier vorgestellt:

/* Arduino-Senso; v1.1
   (c)2012 by Kai Boenke
   Published under CC BY-NC-SA 3.0
 */

// Define Pins
#define ledGrn 6
#define ledRed 7
#define ledBlu 8
#define ledYel 9
#define btnGrn 10
#define btnRed 11
#define btnBlu 12
#define btnYel 13

// Customizations
#define maxWait 100000
#define maxMoves 30
#define inputDelay 500

// Declare Program-internal Vars
boolean waiting; //Defines Status for loop()
int waitCounter; //Used to determine Timeouts in loop()
int moveCounter; //Defines current Move in Sequence (Sequence-Replay)
int nextCounter; //Defines curent move to be made (next move to be made)
int rndSequence[maxMoves]; //Holds complete Sequence
int ledPinMin; //Define lowest Pin
int ledPinMax; //Define highest Pin


// Intial Setup
void setup() {
  // Pinmodes
  pinMode(ledGrn, OUTPUT);
  pinMode(ledRed, OUTPUT);
  pinMode(ledBlu, INPUT); //Exploits reduced voltage to dim LED
  pinMode(ledYel, OUTPUT);
  pinMode(btnGrn, INPUT);
  pinMode(btnRed, INPUT);
  pinMode(btnBlu, INPUT);
  pinMode(btnYel, INPUT);
  
  // Determine lowest/highest LED-Pins
  ledPinMin = min(min(min(ledGrn, ledRed), ledYel), ledBlu);
  ledPinMax = max(max(max(ledGrn, ledRed), ledYel), ledBlu);
  
  // Randomness
  Serial.begin(9600);
  randomSeed(analogRead(0));
  
  // Start first Game
  initGame();
}

// Main Loop
void loop() {
  // Check for Timeout
  if(waitCounter > maxWait){
    initGame();
  }else{
    waitCounter++;
  }
  
  // Show Sequence for new Move?
  if((!waiting) && (moveCounter == 0)){
    showSequence();
  }
  
  // Read Input
  int valGrn = digitalRead(btnGrn);
  int valRed = digitalRead(btnRed);
  int valBlu = digitalRead(btnBlu);
  int valYel = digitalRead(btnYel);
  
  // Evaluate Input
  if((valGrn == HIGH) || (valRed == HIGH) || (valBlu == HIGH) || (valYel == HIGH)){
    switch(rndSequence[moveCounter]){
      case ledGrn:
        if(valGrn == HIGH){
          blink(ledGrn);
          nextTry();
        }else{
          youLoose();
        }
        break;
      case ledRed:
        if(valRed == HIGH){
          blink(ledRed);
          nextTry();
        }else{
          youLoose();
        }
        break;
      case ledBlu:
        if(valBlu == HIGH){
          blink(ledBlu);
          nextTry();
        }else{
          youLoose();
        }
        break;
      case ledYel:
        if(digitalRead(btnYel) == HIGH){
          blink(ledYel);
          nextTry();
        }else{
          youLoose();
        }
        break;
    }
  }
}


/*
   Game-Logic
 */

// Setup a new Game
void initGame(){
  // Re-Initialize
  nextCounter = 0;
  moveCounter = 0;
  waiting = false;
  waitCounter = 0;
  
  // Visualize beginning of new Game
  roundBlink(5);
  
  // Generate new Sequence
  for(int i = 0; i < maxMoves; i++){
    rndSequence[i] = random(ledPinMin, ledPinMax+1);
    delay(20); //Try to increase Randomness even more
  }
}

// Visualize Sequence
void showSequence() {
  // Customize
  int blinkDelay = 500;
  int waitDelay = 100;
  
  // Show Sequence
  resetLeds();
  for(int i=0; i<=nextCounter; i++){
    blink(rndSequence[i], blinkDelay);
    delay(waitDelay);
  }
  
  // Reset Status
  waiting = true;
}

// Advance Try after successful Move
void nextTry(){
  // Next Move in Sequence or Replay?
  if(moveCounter >= nextCounter){
    delay(inputDelay);
    blinkAll(1, 10);
    nextCounter++;
    moveCounter = 0;
  }else{
    moveCounter++;
  }
  
  // Base-Case
  if(nextCounter >= maxMoves){
    // We have a Winner!
    blinkAll(10, 10);
    initGame();
  }else{
    // Move on!
    delay(inputDelay);
    waiting = false;
  }
}

// Wrong Move
void youLoose(){
  // Visualize
  blinkAll(1, 500);
  
  // New Game
  initGame();
}


/*
   LED-Controls
 */

// Single Blink
void blink(int led){
  blink(led, 100);
}
void blink(int led, int blinkDelay){
  resetLeds();
  digitalWrite(led, HIGH);
  delay(blinkDelay);
  digitalWrite(led, LOW);
}

// Random Blink
void blinkRandom(){
  blinkRandom(1, 100, 0);
}
void blinkRandom(int loops){
  blinkRandom(loops, 100, 0);
}
void blinkRandom(int loops, int blinkDelay){
  blinkRandom(loops, blinkDelay, 0);
}
void blinkRandom(int loops, int blinkDelay, int wait) {
  resetLeds();
  for(int i = 0; i

Insgesamt hat mich dieses Projekt wesentlich mehr Nerven als Geld gekostet. Wenn man bereits ein Arduino zur Hand hat, benötigt man lediglich eine Platine, ein paar LEDs, WiderstĂ€nde sowie vier Schalter – den Rest kann man wiederverwenden. Stellt sich nur die Frage was ich jetzt mit dem Arduino anfange…

2266 Tage zuvor: Warum kompliziert...

Es gibt Tage, an denen man die Welt nicht mehr versteht. Letztens war einer dieser Tage. Ich programmierte ein kleines Tool um Labels auf einem Zebra Drucker auszugeben. Dachte ich Anfangs noch das die Erstellung des Layouts in ZPL die grĂ¶ĂŸte Herausforderung werden wĂŒrde, wurde ich alsbald eines besseren belehrt.

ZPL – das sei an dieser Stelle ausdrĂŒcklich erwĂ€hnt – ist eine schnörkellose, schnell erlernbare Beschreibungssprache. Hat man sich erst einmal an die Syntax gewöhnt, kann man in kĂŒrzester Zeit ansprechende Layouts erstellen. Das man die Anweisungen in Klartext via TCP/IP direkt an den Drucker ĂŒbergeben kann, macht es auch sehr angenehm die Schnittstelle in eigenen Programmen zu implementieren – ganz gleich welche Sprache man benutzt.

So verwundert es im Nachhinein nicht wirklich, dass dies nicht die befĂŒrchtete Herausforderung darstellte. Nun sollte das Tool noch Daten aus einem MS SQL Server laden um diese in das Label-Layout einzufĂŒgen. Es ist nicht mein erstes Programm, dass Daten aus einem MS SQL Server laden sollte – und um die Dinge noch einfachere zu machen wĂ€hlte ich VB.NET als meine Programmiersprache. Doch hier begann die Verzweiflung zuzuschlagen…

Da ich keinen direkten Zugriff auf die Datenbank hatte, musste ich den Umweg ĂŒber die SSRS gehen. Naiv wie ich bin, ging ich davon aus das dies kein Problem darstellen sollte – gibt es doch entsprechende Objekte und Klassen im .NET Framework. Und siehe da: Man kann sogar mit wenigen Mausklick eine Referenz auf einen SSRS Server anlegen (wenn man die URL kennt und manuell entsprechend aufbereitet):

ReportingService2005 Web Reference

Aber… da habe ich wohl zu einfach gedacht. Denn trotz mannigfaltiger AnsĂ€tze, dem quer-lesen dutzender Artikel und des testens nahezu jeder Funktion der ReportingService2005-Klasse musste ich nach zwei Tagen aufgeben. Zwar konnte ich den Server ansprechen und auch diverse Aktionen durchfĂŒhren – allein Daten auszulesen war mir nicht möglich. Und darum ging es ja nun schließlich.

Also besann ich mich auf eine althergebrachte Methode: XML. Und ein XML-Export stellt fĂŒr die SSRS kein Problem dar (wenn man die URL entsprechend manuell aufbereitet). Hier also meine kleine Funktion fĂŒr all jene, die gegebenenfalls in dieselbe Bredouille geraten:

Public Function LoadSsrsData() As Boolean
	Dim url As String = _
	 + _
	 + _
	"&rs:Command=Render&rs:Format=XML&" +  + "=" + 
	Dim httpReq As WebRequest = _
	WebRequest.Create(url)
	With httpReq
		.Credentials = CredentialCache.DefaultCredentials
	End With

	Dim httpDoc As WebResponse
	Try
		httpdoc = httpReq.GetResponse()
	Catch ex As Exception
		Debug.Print(ex.Message)
		Return False
	End Try

	Dim xmlDoc As New XmlDocument()
	Dim xmlNodes As XmlNodeList
	Dim xmlNode As XmlNode
	Dim xmlAttr As XmlAttribute
	Try
		xmlDoc.Load(httpDoc.GetResponseStream())
		Debug.Print(xmlDoc.InnerXml)
	Catch ex As Exception
		Debug.Print(ex.Message)
		Return False
	End Try

	Return True
End Function

2388 Tage zuvor: ZĂ€hlen fĂŒr AnfĂ€nger

UnglĂŒcklicher Weise musste ich heute eine Datenbank migrieren – oder eigentlich eher kopieren. Denn so ein SQL Server Express bietet leider wenig Möglichkeiten die Daten strukturiert zu extrahieren. Nachdem ich also die Datenbank mit dem Microsoft SQL Server Management Studio auf einen richtigen SQL Server umziehen konnte, wurde ich mit einem merkwĂŒrdigen, nicht aussagekrĂ€ftigen Status abgespeist:

SQL Server Import: Copying Stopped?

Stopped ist weder Fisch noch Fleisch, daher wollte ich zumindest die Anzahl der DatensÀtze vergleichen. Wie sich herausstellte ist das gar nicht mal so einfach. Aber mit ein wenig Tricksereien (und SQL Injection) geht auch dies:

use myDataBase
declare @TotalNumRows int, @CurNumRows int, @CurTableName varchar(50), @sql varchar(4000)
declare TotalNumRows_Cursor Cursor for
select table_name from information_schema.tables where table_type='BASE TABLE'
set @TotalNumRows = 0

open TotalNumRows_Cursor
fetch next from TotalNumRows_Cursor
while @@FETCH_STATUS = 0
begin
	fetch next from TotalNumRows_Cursor into @CurTableName
	select @sql = 'select * from [' + @CurTableName +']'
	exec(@sql)
	select @TotalNumRows = @TotalNumRows + @@RowCount
End
close TotalNumRows_Cursor
Print @TotalNumRows
deallocate TotalNumRows_Cursor

go

Achja, bei besagtem SQL Server Express kann man sich ĂŒbrigens mit dem kleinen Programm qsql.exe an der Datenbank via SQL Prompt anmelden. Auch das muss man erst einmal wissen.

2440 Tage zuvor: Bosch bei Channel9

Über was man manchmal so stolpert:

Leider wird Silverlight fĂŒr die Darstellung benötigt.

2521 Tage zuvor: Ein Filesystem in Excel

Vor kurzem musste ich die Datei- und Ordner-Struktur unserer File-Server abbilden (mit Zeitstempel der letzten Änderung). Idealer Weise in einer Form, in der man die Daten in einer Tabellenkalkulation weiter bearbeiten konnte. Mit Standard-Mitteln war zu viel manueller Aufwand involviert und die ĂŒblichen Skriptsprachen waren zu unflexibel. Daher habe ich meine reichlich angestaubten Java-FĂ€higkeiten bemĂŒht und ein kleines Tool dafĂŒr geschrieben:

public class FileCrawler{
public static void main(String args[]){
	String root = ".";
	if(args.length > 0)
		root = args[0];
	FileCrawler fc = new FileCrawler(root);
}

java.io.FileWriter fout;
java.text.SimpleDateFormat dformat;

public FileCrawler(String root){
	try{
		this.fout = new java.io.FileWriter("FileCrawler.txt");
	} catch(java.io.IOException e){
		System.err.println("Could not write to FileCrawler.txt, using Console...");
	}
	this.dformat = new java.text.SimpleDateFormat("M/d/y  h:m:s a");
	
	this.crawl(root);
	
	try{ this.fout.close(); } catch(java.io.IOException e){ }
}

private void crawl(String path){
	java.io.File root = new java.io.File(path);
	if(!root.exists() && !root.isDirectory()){
		System.err.println("Invalid path: " + path);
		return;
	}

	try{
		java.io.File files[] = root.listFiles();
		for(int i = 0; i < files.length; i++){
			if(files[i].isDirectory())
				this.crawl(root.getPath() + java.io.File.separator + files[i].getName());
			else
				this.writeFile(path, files[i].getName(), files[i].lastModified());
		}
	} catch(java.lang.Exception e){
		System.err.println("Access denied: " + path);
	}
}

private void writeFile(String path, String file, long timestamp){
	String modified = this.dformat.format(new java.util.Date(timestamp));
	String msg = path + "\t" + file + "\t" + modified;
	try{
		this.fout.write(msg + "\n");
	} catch(java.io.IOException e){
		System.out.println(msg);;
	}
}
}

Es nimmt einen zu scannenden Pfad als Parameter entgegen und schreibt die Ergebnisse als CSV-Datei weg. Vielleicht hilft es ja jemandem weiter. Und bedenken: Excel 2003 kann nur bis zu 46500 Zeilen verwalten!

2781 Tage zuvor: PHP fĂŒttern

Vor geraumer Zeit hatte ich schon mal eine RSS-Reader Klasse in PHP vorgestellt. Wie ich mittlerweile gelernt habe, ist es mit PHP5 nicht mehr nötig selbst dem Code auszulesen und zu interpretieren. Es gibt dort die schöne Klasse DOMDocument, die einem diese Arbeit abnimmt. Und das auch noch recht elegant:

$reader = new DOMDocument();
$reader->load($url) || die('Could not read Feed');
foreach($reader->getElementsByTagName('entry') as $item)
	$data[] = $item->getElementsByTagName('title')->item(0)->childNodes->item(0)->nodeValue;

Et voilĂ , man hat alle Titel eines RSS-Feed in seinem Array versammelt. Praktisch! Gedacht ist die Klasse fĂŒr XML, daher kann man damit noch sehr viel mehr anstellen. Zum Beispiel SOAP. Aber dazu spĂ€ter mehr.

2799 Tage zuvor: GeoIP-Lookup in GeoRSS umwandeln

Um die Frage zu beantworten wie ich denn IP-Adressen in eine Google-Karte bekommen habe: Mittels geoip, GeoRSS und (logisch) Google-Maps. Und zwar folgendermaßen.

  • geoiplookup <IP> >> geolookup.txt speichert die Ausgabe in einer Datei.
  • Mit regulĂ€ren AusdrĂŒcken extrahiert man die LĂ€ngen- und Breitenangaben.
  • Diese legt man als GeoRSS-Feed ab.
  • Den Feed wiederum kann man in Google-Maps importieren.

Fertig ist die Karte. Wer es ein wenig einfacher haben will, kann auch folgendes PHP-Script zum umwandeln der geoip-Ausgabe in einen GeoRSS-Feed nutzen.


	
	
	
	
		

%s, %s, %s%s %s\n"; $geodata['points'] = array(); $geodata['rawdata'] = explode("\n", $_POST['geoiplookup']); // XML-Special Characters-Function (alonso05 at gmail dot com) function xml_character_encode($string, $trans=''){ $trans = (is_array($trans)) ? $trans : get_html_translation_table(HTML_ENTITIES, ENT_QUOTES); foreach ($trans as $k=>$v) $trans[$k]= "&#".ord($k).";"; return strtr($string, $trans); } // Parse input foreach($geodata['rawdata'] as $data) if(preg_match_all($regex, $data, $results)) $geodata['points'][] = array( xml_character_encode(htmlentities($results[3][0])), xml_character_encode(htmlentities($results[2][0])), xml_character_encode(htmlentities($results[1][0])), $results[5][0], $results[6][0] ); // Create GeoRSS-Feed header('Content-type: application/xml'); echo(''); echo(''); foreach($geodata['points'] as $point) vprintf($geodata['line'], $point); echo(''); ?>

2801 Tage zuvor: Fern-Fernsehen

Nachdem das ZDF seine Mediathek umgestaltet hat, funktionierte mein schöner Fern-Fernseher leider nicht mehr richtig. Das entsprechende Plug-In wurde nÀmlich leider nicht auf die neue Struktur umgestellt. Doch wie meinte mein alter Abteilungsleiter (streng genommen war es seine Frau) doch gleich? Darfst kein Depp sein. Ergo, selbst ist der Mann:

#!/bin/sh
d=`date +%y%m%d`
j="/media/public/heute_journal_$d.asx"
h="/media/public/heute_$d.asx"
s="/media/public/heuteshow$d.asx"

rm /media/public/heute_*.asx

wget -qO $j "http://wstreaming.zdf.de/zdf/veryhigh/"$d"_hjo.asx"
[ "$?" -ne "0" ] && \
        rm $j

wget -qO $h "http://wstreaming.zdf.de/zdf/veryhigh/"$d"_h19.asx"
[ "$?" -ne "0" ] && \
        rm $h

wget -qO $s "http://wstreaming.zdf.de/zdf/veryhigh/"$d"_heuteshow_hsh.asx"
[ "$?" -ne "0" ] && \
        rm $s

Das kleine Script lĂ€uft bei mir tĂ€glich um 18:00h (Mitternacht deutscher Zeit) und lĂ€dt die kleinen Streaming-Links herunter. Die wiederum kann XBMC problemlos abspielen. Leider bekommt man so nur die Auslands-Fassung mit (aus rechtlichen GrĂŒnden) weniger BeitrĂ€gen und an manchen Stellen ohne Bild. Aber immerhin.

2811 Tage zuvor: Google-Maps Suche

Schon interessant, wie man manchmal bei eigentlich selbstverstĂ€ndlichen Sachen forschen und tricksen muss. Zuletzt bei der Anzeige von diversen Adressen in einer Karte von Google-Maps. Hier sollte der Kartenausschnitt auf die angezeigten Ergebnisse zentriert und der Zoom entsprechend gesetzt werden. Standard-Funktionen gibt es dafĂŒr leider nicht, aber mit einem kleinen Trick funktioniert es dennoch recht geschickt:

var map = new GMap2(document.getElementById("map"));
var geocoder = new GClientGeocoder();
map.setCenter(new GLatLng(25.783662, -80.189466), 9);
var viewport = new GLatLngBounds();
GEvent.addListener(map, 'markeradded', function(){
	map.setCenter(viewport.getCenter());
	map.setZoom(map.getBoundsZoomLevel(viewport)-1);
	});
geocoder.getLatLng("1900 N BAYSHORE DR  4109, Miami FL 33132", function(point){
	var marker = new GMarker(point);
	GEvent.addListener(marker, 'click', function(){
		marker.openInfoWindowHtml("1900 N BAYSHORE DR  4109, Miami FL 33132
Show Details"); }); map.addOverlay(marker); if(!viewport.contains(point)){ iewport.extend(point); } GEvent.trigger(map, 'markeradded'); });

Der Trick besteht darin im Eventhandler der Marker den hinzugefĂŒgten Punkt einem GLatLngBounds-Objekt zu ĂŒbergeben. Mit Hilfe dieses Objekts kann der Kartenausschnitt neu angepasst werden.

Interessant ist dabei, dass panTo() durch den direkt anschließenden Aufruf von setZoom() nicht korrekt funktioniert. Mit setCenter() gibt es allerdings keine Probleme.

3682 Tage zuvor: RFC-Umwege

Um die Optionen einen RFC-Verbindung auszuwerten gibt es einen schönen Funktionsbaustein namens RFCDES2RFCDISPLAY. Dieser löst den Connect-String aus Tabelle RFCDES auf und bietet einem die Informationen in Struktur RFCDISPLAY zur weiteren Verarbeitung an. Leider musste ich jedoch feststellen, dass sowohl die Load-Balancing Informationen der RFC-Verbindung selbst, als auch die der dazugehörigen A-RFC Einstellungen nicht korrekt ĂŒbergeben werden.

Da ich weder im SDN noch in den sonstigen weiten des Internets fĂŒndig geworden bin, möchte ich auf diese Weise meinen Work-Around mit dem Rest der Welt teilen:

* ----- Get RFC-Details (Options) -------------------------------------*
Form get_rfcoptions using     value(dest) type RFCDES-RFCDEST
                    changing  options type RFCDISPLAY.
*       Temporary variables
  Data:         rfcdest       like rfcdes,
        Begin of it_rfcopt occurs 32,
                token         like rfcdes-rfcoptions,
        End of it_rfcopt.
*       Get RFC-Destination
  Select single * from RFCDES into rfcdest where RFCDEST = dest.
*       Get Details
  Call function 'RFCDES2RFCDISPLAY'
                exporting     import_rfcdes = rfcdest
                importing     export_rfcdisplay = options.
*       Check Load-Balancing (not properly checked in Function-Module)
  Split rfcdest-rfcoptions at ',' into table it_rfcopt.
  Loop at it_rfcopt where token = 'X=LB=ON'.
    options-rfclbflag = true.
  EndLoop.
*       Check A-RFC Options (not checked in Function-Module)
  Select * from RFCCHECK where rfcdest = dest.
    If RFCCHECK-ARFCACTIVE = true.
      options-rfcarfcopt = true.
    EndIf.
  EndSelect.
EndForm.

3947 Tage zuvor: Passwörter-Raten

Als Administrator weist man ab und an seine Anwender darauf hin komplexere Passwörter zu nutzen, die aus einer zufĂ€lligen Reihenfolge von Buchstaben und Zahlen bestehen. Innerhalb eines SAP-System pflegt man denn auch einige Passwortregeln in Bezug auf deren LĂ€nge oder einiger Wörter und Wortteile, die nicht in Passwörtern vorkommen dĂŒrfen. Und man selbst geht natĂŒrlich mit leuchtendem Beispiel voran und verwendet ausschließlich kryptische Passwörter.

Dumm nur, wenn man dann die Passwortliste fĂŒr technische Hintergrund-User verliert. Da diese User meist fĂŒr die Anbindung anderer Systeme genutzt werden, kann man auch deren Passwort nicht einfach wieder zurĂŒcksetzen. Grund genug also, sich einmal nĂ€hergehend mit der PasswortverschlĂŒsselung von SAP zu beschĂ€ftigen.

ZunĂ€chst einmal das Wichtigste: Passwörter werden innerhalb eines SAP-Systems mit einem nicht weiter bekannten Algorhytmus im Kernel selbst verschlĂŒsselt. Dabei wird der Username mit in die Berechnung einbezogen, wodurch ein anderer User mit demselben Passwort einen anderen Hash-String bekĂ€me. Der so verschlĂŒsselte Hash-String wird anschließend in Tabelle USR02 zu einem User abgelegt.

Wie die Passwörter genau verschlĂŒsselt werden ist nicht bekannt. Allerdings können wir uns zunutze machen, dass die SID eines Systems nicht mit in die Berechnung eingeschlossen wird. Dadurch können wir auf einem anderen System, auf dem der User nicht existiert, einen Report starten, der die möglichen Passwort-Kombinationen durchtestest. Der Report benutzt einen Funktionsbaustein, der einen User mit einem gegebenen Namen und Passwort anlegt. Nach der Anlage kann nun der Hash-Wert ausgelesen und mit einem anderen Wert verglichen werden. Ist der Hash-Wert derselbe, hat man das Passwort gefunden:

REPORT ZBOE_TEST .
parameters: user    like usr02-bname,
            pw_hash like usr02-bcode.

data: charset type table of c with default key initial size 26,
      char1 like line of charset,
      char2 like line of charset,
      char3 like line of charset,
      char4 like line of charset,
      char5 like line of charset,
      char6 like line of charset,
      char7 like line of charset,
      char8 like line of charset,
      pass(8),
      logondata type USLOGOND.

CALL FUNCTION 'SUSR_USER_CREATE'
  EXPORTING
    USER_NAME                          = user
    USER_LOGONDATA                     = logondata
    PASSWORD                           = ''
  EXCEPTIONS
    USER_NAME_ALREADY_EXISTS           = 1
    USER_LOCKED_BY_ANOTHER_ADMIN       = 2
    PASSWORD_NOT_ALLOWED               = 3
    ERROR_IN_LOCK_MANAGEMENT           = 4
    ERROR_WRITING_TO_DB                = 5
    INTERNAL_ERROR                     = 6
    OTHERS                             = 7.
case SY-SUBRC.
  when 0.
    break 1.
  when 1.
    MESSAGE e000(fb) with 'User existiert bereits!'.
  when others.
    MESSAGE e000(fb) with 'User konnte nicht angelegt werden.'.
endcase.

append 'A' to charset.
append 'B' to charset.
append 'C' to charset.
append 'D' to charset.
append 'E' to charset.
append 'F' to charset.
append 'G' to charset.
append 'H' to charset.
append 'I' to charset.
append 'J' to charset.
append 'K' to charset.
append 'L' to charset.
append 'M' to charset.
append 'N' to charset.
append 'O' to charset.
append 'P' to charset.
append 'Q' to charset.
append 'R' to charset.
append 'S' to charset.
append 'T' to charset.
append 'U' to charset.
append 'V' to charset.
append 'W' to charset.
append 'X' to charset.
append 'Y' to charset.
append 'Z' to charset.

loop at charset into char1.
  loop at charset into char2.
    loop at charset into char3.
      loop at charset into char4.
        loop at charset into char5.
          loop at charset into char6.
            loop at charset into char7.
              loop at charset into char8.
                 concatenate char1 char2 char3 char4
                             char5 char6 char7 char8 into pass.
                 CALL FUNCTION 'SUSR_USER_CHANGE'
                   EXPORTING
                     USER_NAME                 = user
                     PASSWORD                  = pass
                   EXCEPTIONS
                     USER_NAME_NOT_EXISTS      = 1
                     PASSWORD_NOT_ALLOWED      = 2
                     INTERNAL_ERROR            = 3
                     OTHERS                    = 4.
                 IF SY-SUBRC <> 0.
                   exit.
                 ENDIF.
                 CALL FUNCTION 'SUSR_USER_LOGONDATA_GET'
                   EXPORTING
                     USER_NAME                 = user
                   IMPORTING
                     USER_LOGONDATA            = logondata.
                 if pw_hash EQ logondata-BCODE.
                   write pass.
                   break 9.
                 endif.
              endloop.
            endloop.
          endloop.
        endloop.
      endloop.
    endloop.
  endloop.
endloop.

data ret_table type table of BAPIRET2.
CALL FUNCTION 'BAPI_USER_DELETE'
  EXPORTING
    USERNAME       = user
  TABLES
    RETURN         = ret_table.

Die Laufzeiten bei diesem Verfahren sind jedoch immens; auch wenn der Report selbst, der hier nur als Proof-of-Concept verstanden werden will, durchaus noch einiges Optimierungspotential besitzt. Die Laufzeit steigt jedoch expotentiell zur Anzahl der verwendeten Zeichen. Die möglichen Kombinationen lassen sich einfach berechnen:

<Anzahl Zeichen>^<PasswortlÀnge>

In einem Test habe ich ein bekanntes Passwort bei einer PasswortlĂ€nge von acht Zeichen und sieben verschiedenen Zeichen in fĂŒnf bis zehn Minuten finden können.

3975 Tage zuvor: Lokalisierung: EqDKP-Raid Progression

So ein DKP-System ist schon was feines: Je nachdem an wie vielen Raids man teilgenommen hat, hat man auch bessere/schlechtere Chancen an ein begehrtes Item zu gelangen. Die Implementation beim Kreis der Macht ist, nicht zuletzt durch einen sehr vernarrten Gildenleiter, auch entsprechend aufgebohrt – schließlich ist es die Statistik-Seite Nummer Eins der Gilde.

Eine der neuesten Erweiterungen, die eingebunden werden sollte, war das PlugIn Raid Progression zur Verfolgung des Fortschritts in Instanzen. Leider Gottes kam es jedoch nicht mit unserer Art des Eintragens von Raids zurecht. Ein Problem, das scheinbar auch andere Leute haben:

looks nice.
is there a change to modify the plugin, so ite can handel with other Raidnotes like this http://www.seniorenraid.de/set/listraids.php

Nun, die Lösung soll niemandem vorbehalten bleiben:

// Extension start

if(empty($row['raid_note']))   continue;

$elements = explode(" ", str_replace(", ", " ", $row['raid_note']));
foreach($elements as $killed_mob)
   foreach($instance as $event => $mobs)
      foreach($mobs as $mob => $count)
         if(@strstr($mob, $killed_mob))
            $instance[$event][$mob] = $row['raid_id'] . ':' . $row['event_id'];

/* Old code
   if ( in_array_multi_key($row['raid_note'], $instance) )
   {

      $instance[$row['raid_name']][$row['raid_note']] = $row['raid_id'] . ':' . $row['event_id'];
   }
*/
// Extension end

Einzubauen ist das ganze in die Datei /dkp/plugins/rp/index.php. Die Stelle sollte sich dem geneigten Leser aus dem auskommentierten Code von selbst erschließen.