summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/SemanticResultFormats/src
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/SemanticResultFormats/src
first commit
Diffstat (limited to 'www/wiki/extensions/SemanticResultFormats/src')
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/BibTex/BibTexFileExportPrinter.php190
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/BibTex/Item.php185
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/BibTex/README.md91
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/Graph/GraphPrinter.php419
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/Outline/ListTreeBuilder.php162
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineItem.php62
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineResultPrinter.php139
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineTree.php90
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/Outline/README.md53
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/Outline/TemplateBuilder.php156
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/ResourceFormatter.php107
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/iCalendar/IcalTimezoneFormatter.php183
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/iCalendar/iCalendarFileExportPrinter.php368
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/vCard/Address.php87
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/vCard/Email.php50
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/vCard/Tel.php51
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/vCard/vCard.php250
-rw-r--r--www/wiki/extensions/SemanticResultFormats/src/vCard/vCardFileExportPrinter.php433
18 files changed, 3076 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticResultFormats/src/BibTex/BibTexFileExportPrinter.php b/www/wiki/extensions/SemanticResultFormats/src/BibTex/BibTexFileExportPrinter.php
new file mode 100644
index 00000000..aa2bc011
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/BibTex/BibTexFileExportPrinter.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace SRF\BibTex;
+
+use SMWTimeValue as TimeValue;
+use SMWQueryResult as QueryResult;
+use SMW\Query\ResultPrinters\FileExportPrinter;
+
+/**
+ * Printer class for creating BibTeX exports
+ *
+ * For details on availble keys see the README
+ *
+ * Example of a book :
+ *
+ * @Book{abramowitz1964homf,
+ * author = "Milton Abramowitz and Irene A. Stegun",
+ * title = "Handbook of Mathematical Functions",
+ * publisher = "Dover",
+ * year = 1964,
+ * address = "New York",
+ * edition = "ninth Dover printing, tenth GPO printing"
+ * }
+ *
+ * @license GNU GPL v2+
+ * @since 1.5
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author Frank Dengler
+ * @author Steren Giannini
+ */
+class BibTexFileExportPrinter extends FileExportPrinter {
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return wfMessage( 'srf_printername_bibtex' )->text();
+ }
+
+ /**
+ * @see FileExportPrinter::getMimeType
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getMimeType( QueryResult $queryResult ) {
+ return 'text/bibtex';
+ }
+
+ /**
+ * @see FileExportPrinter::getFileName
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFileName( QueryResult $queryResult ) {
+
+ if ( $this->params['filename'] !== '' ) {
+
+ if ( strpos( $this->params['filename'], '.bib' ) === false ) {
+ $this->params['filename'] .= '.bib';
+ }
+
+ return str_replace( ' ', '_', $this->params['filename'] );
+ } elseif ( $this->getSearchLabel( SMW_OUTPUT_WIKI ) != '' ) {
+ return str_replace( ' ', '_', $this->getSearchLabel( SMW_OUTPUT_WIKI ) ) . '.bib';
+ }
+
+ return 'BibTeX.bib';
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['filename'] = [
+ 'message' => 'smw-paramdesc-filename',
+ 'default' => 'bibtex.bib',
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param array $list
+ *
+ * @return string
+ */
+ public function getFormattedList( $key, array $values ) {
+ return $GLOBALS['wgLang']->listToText( $values );
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ if ( $outputMode !== SMW_OUTPUT_FILE ) {
+ return $this->getBibTexLink( $res, $outputMode );
+ }
+
+ $items = [];
+
+ while ( $row = $res->getNext() ) {
+ $items[] = $this->newItem( $row )->text();
+ }
+
+ return implode( "\r\n\r\n", $items );
+ }
+
+ private function getBibTexLink( QueryResult $res, $outputMode ) {
+
+ // Can be viewed as HTML if requested, no more parsing needed
+ $this->isHTML = $outputMode == SMW_OUTPUT_HTML;
+
+ $link = $this->getLink(
+ $res,
+ $outputMode
+ );
+
+ return $link->getText( $outputMode, $this->mLinker );
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $row array of SMWResultArray
+ *
+ * @return bibTexItem
+ */
+ private function newItem( array /* of SMWResultArray */ $row ) {
+
+ $item = new Item();
+ $item->setFormatterCallback( [ $this, 'getFormattedList' ] );
+
+ foreach ( $row as /* SMWResultArray */ $field ) {
+ $printRequest = $field->getPrintRequest();
+ $values = [];
+
+ $label = strtolower( $printRequest->getLabel() );
+ $dataValue = $field->getNextDataValue();
+
+ if ( $dataValue === false ) {
+ continue;
+ }
+
+ if ( $label === 'date' && $dataValue instanceof TimeValue ) {
+ $item->set( 'year', $dataValue->getYear() );
+ $item->set( 'month', $dataValue->getMonth() );
+ } elseif ( $label === 'author' || $label === 'authors' ) {
+ $values[] = $dataValue->getShortWikiText();
+
+ while ( ( /* SMWDataValue */ $dataValue = $field->getNextDataValue() ) !== false ) {
+ $values[] = $dataValue->getShortWikiText();
+ }
+
+ $item->set( 'author', $values );
+ } elseif ( $label === 'editor' || $label === 'editors' ) {
+ $values[] = $dataValue->getShortWikiText();
+
+ while ( ( /* SMWDataValue */ $dataValue = $field->getNextDataValue() ) !== false ) {
+ $values[] = $dataValue->getShortWikiText();
+ }
+
+ $item->set( 'editor', $values );
+ } else {
+ $item->set( $label, $dataValue->getShortWikiText() );
+ }
+ }
+
+ return $item;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/BibTex/Item.php b/www/wiki/extensions/SemanticResultFormats/src/BibTex/Item.php
new file mode 100644
index 00000000..7ff631ef
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/BibTex/Item.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace SRF\BibTex;
+
+use SMWDataValue as DataValue;
+
+/**
+ * @see http://www.semantic-mediawiki.org/wiki/BibTex
+ *
+ * @license GNU GPL v2+
+ * @since 3.1
+ *
+ * @author mwjames
+ */
+class Item {
+
+ /**
+ * @see https://en.wikipedia.org/wiki/BibTeX
+ *
+ * @var string
+ */
+ private $type = '';
+
+ /**
+ * @var []
+ */
+ protected $fields = [
+ 'address' => '',
+ 'annote' => '',
+ 'author' => [],
+ 'booktitle' => '',
+ 'chapter' => '',
+ 'crossref' => '',
+ 'doi' => '',
+ 'edition' => '',
+ 'editor' => [],
+ 'eprint' => '',
+ 'howpublished' => '',
+ 'institution' => '',
+ 'journal' => '',
+ 'key' => '',
+ 'month' => '',
+ 'note' => '',
+ 'number' => '',
+ 'organization' => '',
+ 'pages' => '',
+ 'publisher' => '',
+ 'school' => '',
+ 'series' => '',
+ 'title' => '',
+ 'url' => '',
+ 'volume' => '',
+ 'year' => ''
+ ];
+
+ /**
+ * @var callable
+ */
+ private $formatterCallback;
+
+ /**
+ * @since 3.1
+ */
+ public function __construct() {
+ $this->type = 'Book';
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param callable $compoundLabelCallback
+ */
+ public function setFormatterCallback( callable $formatterCallback ) {
+ $this->formatterCallback = $formatterCallback;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $key
+ * @param string $text
+ *
+ * @return string
+ */
+ public function replace( $key, $text ) {
+
+ if ( $key === 'uri' ) {
+ $text = str_replace(
+ [ "Ä", "ä", "Ö", "ö", "Ü", "ü", "ß" ],
+ [ 'Ae', 'ae', 'Oe', 'oe', 'Ue', 'ue', 'ss' ],
+ $text
+ );
+ $text = preg_replace("/[^a-zA-Z0-9]+/", "", $text );
+ }
+
+ return $text;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $key
+ * @param mixed $value
+ */
+ public function set( $key, $value ) {
+
+ $key = strtolower( $key );
+
+ if ( $key === 'type' ) {
+ $this->type = ucfirst( $value );
+ }
+
+ if ( isset( $this->fields[$key] ) ) {
+ $this->fields[$key] = $value;
+ }
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @return string
+ */
+ public function text() {
+
+ $formatterCallback = $this->formatterCallback;
+
+ $text = '@' . $this->type . '{' . $this->buildURI() . ",\r\n";
+
+ foreach ( $this->fields as $key => $value ) {
+
+ if ( ( $key === 'author' || $key === 'editor' ) && is_array( $value ) ) {
+ if ( is_callable( $formatterCallback ) ) {
+ $value = $formatterCallback( $key, $value );
+ } else {
+ $value = implode( ', ', $value );
+ }
+ }
+
+ if ( $value === '' ) {
+ continue;
+ }
+
+ $text .= ' ' . $key . ' = "' . $value . '", ' . "\r\n";
+ }
+
+ $text .= "}";
+
+ return $text;
+ }
+
+ /**
+ * Consist of `author last name` + `year` + `first word of title`
+ *
+ * @return string
+ */
+ protected function buildURI() {
+
+ $uri = '';
+
+ if ( isset( $this->fields['author'] ) ) {
+ foreach ( $this->fields['author'] as $key => $author ) {
+ $elements = explode( ' ', $author );
+ $uri .= array_pop( $elements );
+ break;
+ }
+ }
+
+ if ( isset( $this->fields['year'] ) ) {
+ $uri .= $this->fields['year'];
+ }
+
+ if ( isset( $this->fields['title'] ) ) {
+ foreach ( explode( ' ', $this->fields['title'] ) as $titleWord ) {
+ $charsTitleWord = preg_split( '//', $titleWord, -1, PREG_SPLIT_NO_EMPTY );
+
+ if ( !empty( $charsTitleWord ) ) {
+ $uri .= $charsTitleWord[0];
+ }
+ }
+ }
+
+ return strtolower( $this->replace( 'uri', $uri ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/BibTex/README.md b/www/wiki/extensions/SemanticResultFormats/src/BibTex/README.md
new file mode 100644
index 00000000..1feab534
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/BibTex/README.md
@@ -0,0 +1,91 @@
+Info is copied from http://en.wikipedia.org/wiki/Bibtex
+
+## Fields
+
+- address: Publisher's address (usually just the city, but can be the full address for lesser-known publishers)
+- annote: An annotation for annotated bibliography styles (not typical)
+- author: The name(s) of the author(s) (in the case of more than one author, separated by and)
+- booktitle: The title of the book, if only part of it is being cited
+- chapter: The chapter number
+- crossref: The key of the cross-referenced entry
+- doi: The DOI number of the entry
+- edition: The edition of a book, long form (such as "first" or "second")
+- editor: The name(s) of the editor(s)
+- eprint: A specification of an electronic publication, often a preprint or a technical report
+- howpublished: How it was published, if the publishing method is nonstandard
+- institution: The institution that was involved in the publishing, but not necessarily the publisher
+- journal: The journal or magazine the work was published in
+- key: A hidden field used for specifying or overriding the alphabetical order of entries (when the "author" and "editor" fields are missing). Note that this is very different from the key (mentioned just after this list) that is used to cite or cross-reference the entry.
+- month: The month of publication (or, if unpublished, the month of creation)
+- note: Miscellaneous extra information
+- number: The "number" of a journal, magazine, or tech-report, if applicable. (Most publications have a "volume", but no "number" field.)
+- organization: The conference sponsor
+- pages: Page numbers, separated either by commas or double-hyphens. For books, the total number of pages.
+- publisher: The publisher's name
+- school: The school where the thesis was written
+- series: The series of books the book was published in (e.g. "The Hardy Boys" or "Lecture Notes in Computer Science")
+- title: The title of the work
+- type: The type of tech-report, for example, "Research Note"
+- url: The WWW address
+- volume: The volume of a journal or multi-volume book
+- year: The year of publication (or, if unpublished, the year of creation)
+
+
+## Types
+
+- article:
+ An article from a journal or magazine.
+ Required fields: author, title, journal, year
+ Optional fields: volume, number, pages, month, note, key
+- book:
+ A book with an explicit publisher.
+ Required fields: author/editor, title, publisher, year
+ Optional fields: volume, series, address, edition, month, note, key, pages
+- booklet:
+ A work that is printed and bound, but without a named publisher or sponsoring institution.
+ Required fields: title
+ Optional fields: author, howpublished, address, month, year, note, key
+- conference:
+ The same as inproceedings, included for Scribe compatibility.
+ Required fields: author, title, booktitle, year
+ Optional fields: editor, pages, organization, publisher, address, month, note, key
+- inbook:
+ A part of a book, usually untitled. May be a chapter (or section or whatever) and/or a range of pages.
+ Required fields: author/editor, title, chapter/pages, publisher, year
+ Optional fields: volume, series, address, edition, month, note, key
+- incollection:
+ A part of a book having its own title.
+ Required fields: author, title, booktitle, year
+ Optional fields: editor, pages, organization, publisher, address, month, note, key
+- inproceedings:
+ An article in a conference proceedings.
+ Required fields: author, title, booktitle, year
+ Optional fields: editor, pages, organization, publisher, address, month, note, key
+- manual:
+ Technical documentation.
+ Required fields: title
+ Optional fields: author, organization, address, edition, month, year, note, key
+- mastersthesis:
+ A Master's thesis.
+ Required fields: author, title, school, year
+ Optional fields: address, month, note, key
+- misc:
+ For use when nothing else fits.
+ Required fields: none
+ Optional fields: author, title, howpublished, month, year, note, key
+- phdthesis:
+ A Ph.D. thesis.
+ Required fields: author, title, school, year
+ Optional fields: address, month, note, key
+- proceedings:
+ The proceedings of a conference.
+ Required fields: title, year
+ Optional fields: editor, publisher, organization, address, month, note, key
+- techreport:
+ A report published by a school or other institution, usually numbered within a series.
+ Required fields: author, title, institution, year
+ Optional fields: type, number, address, month, note, key
+- unpublished:
+ A document having an author and title, but not formally published.
+ Required fields: author, title, note
+ Optional fields: month, year, key
diff --git a/www/wiki/extensions/SemanticResultFormats/src/Graph/GraphPrinter.php b/www/wiki/extensions/SemanticResultFormats/src/Graph/GraphPrinter.php
new file mode 100644
index 00000000..91def23d
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/Graph/GraphPrinter.php
@@ -0,0 +1,419 @@
+<?php
+
+namespace SRF\Graph;
+
+use SMWResultPrinter;
+use SMWQueryResult;
+use SMWDataValue;
+use SMWWikiPageValue;
+
+/**
+ * SMW result printer for graphs using graphViz.
+ * In order to use this printer you need to have both
+ * the graphViz library installed on your system and
+ * have the graphViz MediaWiki extension installed.
+ *
+ * @file SRF_Graph.php
+ * @ingroup SemanticResultFormats
+ *
+ * @licence GNU GPL v2+
+ * @author Frank Dengler
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class GraphPrinter extends SMWResultPrinter {
+
+ const NODELABEL_DISPLAYTITLE = 'displaytitle';
+
+ public static $NODE_LABELS = [
+ self::NODELABEL_DISPLAYTITLE,
+ ];
+
+ public static $NODE_SHAPES = [
+ 'box',
+ 'box3d',
+ 'circle',
+ 'component',
+ 'diamond',
+ 'doublecircle',
+ 'doubleoctagon',
+ 'egg',
+ 'ellipse',
+ 'folder',
+ 'hexagon',
+ 'house',
+ 'invhouse',
+ 'invtrapezium',
+ 'invtriangle',
+ 'Mcircle',
+ 'Mdiamond',
+ 'Msquare',
+ 'none',
+ 'note',
+ 'octagon',
+ 'parallelogram',
+ 'pentagon ',
+ 'plaintext',
+ 'point',
+ 'polygon',
+ 'rect',
+ 'rectangle',
+ 'septagon',
+ 'square',
+ 'tab',
+ 'trapezium',
+ 'triangle',
+ 'tripleoctagon',
+ ];
+
+ protected $m_graphName;
+ protected $m_graphLabel;
+ protected $m_graphColor;
+ protected $m_graphLegend;
+ protected $m_graphLink;
+ protected $m_rankdir;
+ protected $m_graphSize;
+ protected $m_labelArray = [];
+ protected $m_graphColors = [
+ 'black',
+ 'red',
+ 'green',
+ 'blue',
+ 'darkviolet',
+ 'gold',
+ 'deeppink',
+ 'brown',
+ 'bisque',
+ 'darkgreen',
+ 'yellow',
+ 'darkblue',
+ 'magenta',
+ 'steelblue2' ];
+ protected $m_nameProperty;
+ protected $m_nodeShape;
+ protected $m_parentRelation;
+ protected $m_wordWrapLimit;
+ protected $m_nodeLabel;
+
+ /**
+ * (non-PHPdoc)
+ * @see SMWResultPrinter::handleParameters()
+ */
+ protected function handleParameters( array $params, $outputmode ) {
+ parent::handleParameters( $params, $outputmode );
+
+ $this->m_graphName = trim( $params['graphname'] );
+ $this->m_graphSize = trim( $params['graphsize'] );
+
+ $this->m_graphLegend = $params['graphlegend'];
+ $this->m_graphLabel = $params['graphlabel'];
+
+ $this->m_rankdir = strtoupper( trim( $params['arrowdirection'] ) );
+
+ $this->m_graphLink = $params['graphlink'];
+ $this->m_graphColor = $params['graphcolor'];
+
+ $this->m_nameProperty = $params['nameproperty'] === false ? false : trim( $params['nameproperty'] );
+
+ $this->m_parentRelation = strtolower( trim( $params['relation'] ) ) == 'parent';
+
+ $this->m_nodeShape = $params['nodeshape'];
+ $this->m_wordWrapLimit = $params['wordwraplimit'];
+
+ $this->m_nodeLabel = $params['nodelabel'];
+ }
+
+ protected function getResultText( SMWQueryResult $res, $outputmode ) {
+
+ if ( !class_exists( 'GraphViz' )
+ && !class_exists( '\\MediaWiki\\Extension\\GraphViz\\GraphViz' )
+ ) {
+ wfWarn( 'The SRF Graph printer needs the GraphViz extension to be installed.' );
+ return '';
+ }
+
+ $this->isHTML = true;
+
+ $graphInput = "digraph $this->m_graphName {";
+ if ( $this->m_graphSize != '' ) {
+ $graphInput .= "size=\"$this->m_graphSize\";";
+ }
+ if ( $this->m_nodeShape ) {
+ $graphInput .= "node [shape=$this->m_nodeShape];";
+ }
+ $graphInput .= "rankdir=$this->m_rankdir;";
+
+ while ( $row = $res->getNext() ) {
+ $graphInput .= $this->getGVForItem( $row, $outputmode );
+ }
+
+ $graphInput .= "}";
+
+ // Calls graphvizParserHook function from MediaWiki GraphViz extension
+ $result = $GLOBALS['wgParser']->recursiveTagParse( "<graphviz>$graphInput</graphviz>" );
+
+ if ( $this->m_graphLegend && $this->m_graphColor ) {
+ $arrayCount = 0;
+ $arraySize = count( $this->m_graphColors );
+ $result .= "<P>";
+
+ foreach ( $this->m_labelArray as $m_label ) {
+ if ( $arrayCount >= $arraySize ) {
+ $arrayCount = 0;
+ }
+
+ $color = $this->m_graphColors[$arrayCount];
+ $result .= "<font color=$color>$color: $m_label </font><br />";
+
+ $arrayCount += 1;
+ }
+
+ $result .= "</P>";
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the GV for a single subject.
+ *
+ * @since 1.5.4
+ *
+ * @param array $row
+ * @param $outputmode
+ *
+ * @return string
+ */
+ protected function getGVForItem( array /* of SMWResultArray */
+ $row, $outputmode ) {
+ $segments = [];
+
+ // Loop throught all fields of the record.
+ foreach ( $row as $i => $resultArray ) {
+
+ // Loop throught all the parts of the field value.
+ while ( ( $object = $resultArray->getNextDataValue() ) !== false ) {
+ $propName = $resultArray->getPrintRequest()->getLabel();
+ $isName = $this->m_nameProperty ? ( $i != 0 && $this->m_nameProperty === $propName ) : $i == 0;
+
+ if ( $isName ) {
+ $name = $this->getWordWrappedText( $object->getShortText( $outputmode ), $this->m_wordWrapLimit );
+ }
+
+ if ( !( $this->m_nameProperty && $i == 0 ) ) {
+ $segments[] = $this->getGVForDataValue( $object, $outputmode, $isName, $name, $propName );
+ }
+ }
+ }
+
+ return implode( "\n", $segments );
+ }
+
+ /**
+ * Returns the GV for a single SMWDataValue.
+ *
+ * @since 1.5.4
+ *
+ * @param SMWDataValue $object
+ * @param $outputmode
+ * @param boolean $isName Is this the name that should be used for the node?
+ * @param string $name
+ * @param string $labelName
+ *
+ * @return string
+ */
+ protected function getGVForDataValue( SMWDataValue $object, $outputmode, $isName, $name, $labelName ) {
+ $graphInput = '';
+ $nodeLabel = '';
+ $text = $object->getShortText( $outputmode );
+
+ if ( $this->m_graphLink ) {
+ $nodeLinkURL = "[[" . $text . "]]";
+ }
+
+ $text = $this->getWordWrappedText( $text, $this->m_wordWrapLimit );
+
+ if ( $this->m_nodeLabel === self::NODELABEL_DISPLAYTITLE && $object instanceof SMWWikiPageValue ) {
+ $objectDisplayTitle = $object->getDisplayTitle();
+ if ( !empty( $objectDisplayTitle )) {
+ $nodeLabel = $this->getWordWrappedText( $objectDisplayTitle, $this->m_wordWrapLimit );
+ }
+ }
+
+ if ( $this->m_graphLink ) {
+ if( $nodeLabel === '' ) {
+ $graphInput .= " \"$text\" [URL = \"$nodeLinkURL\"]; ";
+ } else {
+ $graphInput .= " \"$text\" [URL = \"$nodeLinkURL\", label = \"$nodeLabel\"]; ";
+ }
+ }
+
+ if ( !$isName ) {
+ $graphInput .= $this->m_parentRelation ? " \"$text\" -> \"$name\" " : " \"$name\" -> \"$text\" ";
+
+ if ( $this->m_graphLabel || $this->m_graphColor ) {
+ $graphInput .= ' [';
+
+ if ( array_search( $labelName, $this->m_labelArray, true ) === false ) {
+ $this->m_labelArray[] = $labelName;
+ }
+
+ $color = $this->m_graphColors[array_search( $labelName, $this->m_labelArray, true )];
+
+ if ( $this->m_graphLabel ) {
+ $graphInput .= "label=\"$labelName\"";
+ if ( $this->m_graphColor ) {
+ $graphInput .= ",fontcolor=$color,";
+ }
+ }
+
+ if ( $this->m_graphColor ) {
+ $graphInput .= "color=$color";
+ }
+
+ $graphInput .= ']';
+
+ }
+
+ $graphInput .= ';';
+ }
+
+ return $graphInput;
+ }
+
+ /**
+ * Returns the word wrapped version of the provided text.
+ *
+ * @since 1.5.4
+ *
+ * @param string $text
+ * @param integer $charLimit
+ *
+ * @return string
+ */
+ protected function getWordWrappedText( $text, $charLimit ) {
+ $charLimit = max( [ $charLimit, 1 ] );
+ $segments = [];
+
+ while ( strlen( $text ) > $charLimit ) {
+ // Find the last space in the allowed range.
+ $splitPosition = strrpos( substr( $text, 0, $charLimit ), ' ' );
+
+ if ( $splitPosition === false ) {
+ // If there is no space (lond word), find the next space.
+ $splitPosition = strpos( $text, ' ' );
+
+ if ( $splitPosition === false ) {
+ // If there are no spaces, everything goes on one line.
+ $splitPosition = strlen( $text ) - 1;
+ }
+ }
+
+ $segments[] = substr( $text, 0, $splitPosition + 1 );
+ $text = substr( $text, $splitPosition + 1 );
+ }
+
+ $segments[] = $text;
+
+ return implode( '\n', $segments );
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see SMWResultPrinter::getName()
+ */
+ public function getName() {
+ return wfMessage( 'srf-printername-graph' )->text();
+ }
+
+ /**
+ * @see SMWResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * @param $definitions array of IParamDefinition
+ *
+ * @return array of IParamDefinition|array
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['graphname'] = [
+ 'default' => 'QueryResult',
+ 'message' => 'srf-paramdesc-graphname',
+ ];
+
+ $params['graphsize'] = [
+ 'type' => 'string',
+ 'default' => '',
+ 'message' => 'srf-paramdesc-graphsize',
+ 'manipulatedefault' => false,
+ ];
+
+ $params['graphlegend'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'srf-paramdesc-graphlegend',
+ ];
+
+ $params['graphlabel'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'srf-paramdesc-graphlabel',
+ ];
+
+ $params['graphlink'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'srf-paramdesc-graphlink',
+ ];
+
+ $params['graphcolor'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'srf-paramdesc-graphcolor',
+ ];
+
+ $params['arrowdirection'] = [
+ 'aliases' => 'rankdir',
+ 'default' => 'LR',
+ 'message' => 'srf-paramdesc-rankdir',
+ 'values' => [ 'LR', 'RL', 'TB', 'BT' ],
+ ];
+
+ $params['nodeshape'] = [
+ 'default' => false,
+ 'message' => 'srf-paramdesc-graph-nodeshape',
+ 'manipulatedefault' => false,
+ 'values' => self::$NODE_SHAPES,
+ ];
+
+ $params['relation'] = [
+ 'default' => 'child',
+ 'message' => 'srf-paramdesc-graph-relation',
+ 'manipulatedefault' => false,
+ 'values' => [ 'parent', 'child' ],
+ ];
+
+ $params['nameproperty'] = [
+ 'default' => false,
+ 'message' => 'srf-paramdesc-graph-nameprop',
+ 'manipulatedefault' => false,
+ ];
+
+ $params['wordwraplimit'] = [
+ 'type' => 'integer',
+ 'default' => 25,
+ 'message' => 'srf-paramdesc-graph-wwl',
+ 'manipulatedefault' => false,
+ ];
+
+ $params['nodelabel'] = [
+ 'default' => '',
+ 'message' => 'srf-paramdesc-nodelabel',
+ 'values' => self::$NODE_LABELS,
+ ];
+
+ return $params;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/Outline/ListTreeBuilder.php b/www/wiki/extensions/SemanticResultFormats/src/Outline/ListTreeBuilder.php
new file mode 100644
index 00000000..f64a4366
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/Outline/ListTreeBuilder.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace SRF\Outline;
+
+use SMW\Query\PrintRequest;
+use SRF\Outline\OutlineTree;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.1
+ *
+ * @author mwjames
+ */
+class ListTreeBuilder {
+
+ /**
+ * @var []
+ */
+ private $params = [];
+
+ /**
+ * @var Linker
+ */
+ private $linker;
+
+ /**
+ * @param array $params
+ */
+ public function __construct( array $params ) {
+ $this->params = $params;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param Linker|null|false $linker
+ */
+ public function setLinker( $linker ) {
+ $this->linker = $linker;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param OutlineTree $tree
+ *
+ * @return string
+ */
+ public function build( OutlineTree $outlineTree ) {
+ return $this->tree( $outlineTree );
+ }
+
+ private function tree( $outline_tree, $level = 0 ) {
+ $text = "";
+
+ if ( !is_null( $outline_tree->items ) ) {
+ $text .= "<ul>\n";
+ foreach ( $outline_tree->items as $item ) {
+ $text .= "<li>{$this->item($item)}</li>\n";
+ }
+ $text .= "</ul>\n";
+ }
+
+ if ( $level > 0 ) {
+ $text .= "<ul>\n";
+ }
+
+ $num_levels = count( $this->params['outlineproperties'] );
+ // set font size and weight depending on level we're at
+ $font_level = $level;
+
+ if ( $num_levels < 4 ) {
+ $font_level += ( 4 - $num_levels );
+ }
+
+ if ( $font_level == 0 ) {
+ $font_size = 'x-large';
+ } elseif ( $font_level == 1 ) {
+ $font_size = 'large';
+ } elseif ( $font_level == 2 ) {
+ $font_size = 'medium';
+ } else {
+ $font_size = 'small';
+ }
+
+ if ( $font_level == 3 ) {
+ $font_weight = 'bold';
+ } else {
+ $font_weight = 'regular';
+ }
+
+ foreach ( $outline_tree->tree as $key => $node ) {
+ $text .= "<p style=\"font-size: $font_size; font-weight: $font_weight;\">$key</p>\n";
+ $text .= $this->tree( $node, $level + 1 );
+ }
+
+ if ( $level > 0 ) {
+ $text .= "</ul>\n";
+ }
+
+ return $text;
+ }
+
+ private function item( $item ) {
+ $first_col = true;
+ $found_values = false; // has anything but the first column been printed?
+ $result = "";
+
+ foreach ( $item->row as $resultArray ) {
+
+ $printRequest = $resultArray->getPrintRequest();
+ $val = $printRequest->getText( SMW_OUTPUT_WIKI, null );
+ $first_value = true;
+
+ if ( in_array( $val, $this->params['outlineproperties'] ) ) {
+ continue;
+ }
+
+ $linker = $this->params['link'] === 'all' ? $this->linker : null;
+
+ if ( $this->params['link'] === 'subject' && $printRequest->isMode( PrintRequest::PRINT_THIS ) ) {
+ $linker = $this->linker;
+ }
+
+ while ( ( $dv = $resultArray->getNextDataValue() ) !== false ) {
+
+ if ( !$first_col && !$found_values ) { // first values after first column
+ $result .= ' (';
+ $found_values = true;
+ } elseif ( $found_values || !$first_value ) {
+ // any value after '(' or non-first values on first column
+ $result .= ', ';
+ }
+
+ if ( $first_value ) { // first value in any column, print header
+ $first_value = false;
+ if ( $this->params['showHeaders'] && ( '' != $printRequest->getLabel() ) ) {
+ $result .= $printRequest->getText( SMW_OUTPUT_WIKI, $linker ) . ' ';
+ }
+ }
+
+ $dataItem = $dv->getDataItem();
+
+ if ( $linker === null && $dataItem->getDIType() === DataItem::TYPE_WIKIPAGE && ( $caption = $dv->getDisplayTitle() ) !== '' ) {
+ $dv->setCaption( $caption );
+ }
+
+ $result .= $dv->getShortText( SMW_OUTPUT_WIKI, $linker );
+ }
+
+ $first_col = false;
+ }
+
+ if ( $found_values ) {
+ $result .= ')';
+ }
+
+ return $result;
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineItem.php b/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineItem.php
new file mode 100644
index 00000000..7b273c25
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineItem.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace SRF\Outline;
+
+/**
+ * Represents a single item, or page, in the outline - contains both the
+ * SMWResultArray and an array of some of its values, for easier aggregation
+ *
+ * @license GNU GPL v2+
+ * @since 3.1
+ */
+class OutlineItem {
+
+ /**
+ * @var [type]
+ */
+ public $row;
+
+ /**
+ * @var []
+ */
+ private $vals;
+
+ /**
+ * @since 3.1
+ *
+ * @param $row
+ */
+ public function __construct( $row ) {
+ $this->row = $row;
+ $this->vals = [];
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $name
+ * @param $value
+ */
+ public function addFieldValue( $key, $value ) {
+ if ( array_key_exists( $key, $this->vals ) ) {
+ $this->vals[$key][] = $value;
+ } else {
+ $this->vals[$key] = [ $value ];
+ }
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $row
+ */
+ public function getFieldValues( $key ) {
+
+ if ( array_key_exists( $key, $this->vals ) ) {
+ return $this->vals[$key];
+ }
+
+ return [ wfMessage( 'srf_outline_novalue' )->text() ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineResultPrinter.php b/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineResultPrinter.php
new file mode 100644
index 00000000..9bb3b042
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineResultPrinter.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace SRF\Outline;
+
+use SMWResultPrinter as ResultPrinter;
+use SMWQueryResult as QueryResult;
+
+/**
+ * A class to print query results in an outline format, along with some
+ * helper classes to handle the aggregation
+ *
+ * @license GNU GPL v2+
+ * @since 1.4.3
+ *
+ * @author Yaron Koren
+ */
+class OutlineResultPrinter extends ResultPrinter {
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return wfMessage( 'srf_printername_outline' )->text();
+ }
+
+ /**
+ * @see SMWResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['outlineproperties'] = [
+ 'islist' => true,
+ 'default' => [],
+ 'message' => 'srf_paramdesc_outlineproperties',
+ ];
+
+ $params[] = [
+ 'name' => 'template',
+ 'message' => 'smw-paramdesc-template',
+ 'default' => '',
+ ];
+
+ $params[] = [
+ 'name' => 'userparam',
+ 'message' => 'smw-paramdesc-userparam',
+ 'default' => '',
+ ];
+
+ $params[] = [
+ 'name' => 'named args',
+ 'type' => 'boolean',
+ 'message' => 'smw-paramdesc-named_args',
+ 'default' => true,
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ // for each result row, create an array of the row itself
+ // and all its sorted-on fields, and add it to the initial
+ // 'tree'
+ $outlineTree = new OutlineTree();
+
+ while ( $row = $res->getNext() ) {
+ $outlineTree->addItem( $this->newOutlineItem( $row ) );
+ }
+
+ // now, cycle through the outline properties, creating the
+ // tree
+ foreach ( $this->params['outlineproperties'] as $property ) {
+ $outlineTree->addProperty( $property );
+ }
+
+ if ( $this->params['template'] !== '' ) {
+ $this->hasTemplates = true;
+ $templateBuilder = new TemplateBuilder(
+ $this->params
+ );
+
+ $templateBuilder->setLinker( $this->mLinker );
+ $result = $templateBuilder->build( $outlineTree );
+ } else {
+ $listTreeBuilder = new ListTreeBuilder(
+ $this->params + [ 'showHeaders' => $this->mShowHeaders ]
+ );
+
+ $listTreeBuilder->setLinker( $this->mLinker );
+ $result = $listTreeBuilder->build( $outlineTree );
+ }
+
+ if ( $this->linkFurtherResults( $res ) ) {
+ $link = $this->getFurtherResultsLink(
+ $res,
+ $outputMode
+ );
+
+ $result .= $link->getText( $outputMode, $this->mLinker ) . "\n";
+ }
+
+ return $result;
+ }
+
+ private function newOutlineItem( $row ) {
+
+ $outlineItem = new OutlineItem( $row );
+
+ foreach ( $row as $field ) {
+ $name = $field->getPrintRequest()->getText( SMW_OUTPUT_HTML );
+
+ if ( !in_array( $name, $this->params['outlineproperties'] ) ) {
+ continue;
+ }
+
+ while ( ( $dataValue = $field->getNextDataValue() ) !== false ) {
+ $outlineItem->addFieldValue(
+ $name,
+ $dataValue->getLongWikiText( $this->getLinker() )
+ );
+ }
+ }
+
+ return $outlineItem;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineTree.php b/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineTree.php
new file mode 100644
index 00000000..3a3ecabb
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/Outline/OutlineTree.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace SRF\Outline;
+
+/**
+ * A tree structure for holding the outline data
+ *
+ * @license GNU GPL v2+
+ * @since 3.1
+ */
+class OutlineTree {
+
+ /**
+ * @var []
+ */
+ public $tree;
+
+ /**
+ * @var []
+ */
+ public $items;
+
+ /**
+ * @var integer
+ */
+ public $itemCount = 0;
+
+ /**
+ * @var integer
+ */
+ public $leafCount = 0;
+
+ /**
+ * @since 3.1
+ *
+ * @param array $items
+ */
+ public function __construct( $items = [] ) {
+ $this->tree = [];
+ $this->items = $items;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $item
+ */
+ public function addItem( $item ) {
+ $this->items[] = $item;
+ $this->itemCount++;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $vals
+ * @param $item
+ */
+ public function categorizeItem( $vals, $item ) {
+ foreach ( $vals as $val ) {
+ if ( array_key_exists( $val, $this->tree ) ) {
+ $this->tree[$val]->items[] = $item;
+ $this->tree[$val]->leafCount++;
+ } else {
+ $this->tree[$val] = new self( [ $item ] );
+ $this->tree[$val]->leafCount++;
+ }
+ }
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param $property
+ */
+ public function addProperty( $property ) {
+ if ( $this->items !== null && count( $this->items ) > 0 ) {
+ foreach ( $this->items as $item ) {
+ $cur_vals = $item->getFieldValues( $property );
+ $this->categorizeItem( $cur_vals, $item );
+ }
+ $this->items = null;
+ } else {
+ foreach ( $this->tree as $i => $node ) {
+ $this->tree[$i]->addProperty( $property );
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/Outline/README.md b/www/wiki/extensions/SemanticResultFormats/src/Outline/README.md
new file mode 100644
index 00000000..62319dfc
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/Outline/README.md
@@ -0,0 +1,53 @@
+# Outline format
+
+The format is to display pages in a hierarchical outline form, using one or more of the pages' properties as outline headers.
+
+## Usage
+
+```
+{{#ask: [[Category:Task]]
+ |?Severity
+ |format=outline
+ |outlineproperties=Severity, Assigned to
+ |template=phab-view
+}}
+```
+
+For example, with `phab-view` as template name, the `outline` format will generate two distinct templates `phab-view-header` and `phab-view-item` to be used for the output with `-header` and `-item` being a fixed affix to distinguish the output level of the result content.
+
+The `...-header` template will provide additional information and includes:
+- `#outlinelevel` the level of the header being processed (depends on the iteration level invoked by the `outlineproperties` parameter)
+- `#itemcount` provides a count information for the items assigned to the
+the level
+- `#userparam`
+
+The `...-item` template will also provide additional information such as:
+
+- `#itemsection` section number of the item
+- `#itemsubject` the subject (aka page) of the item processed
+- `#userparam` content provided by a user via the `#ask` `userparam` parameter
+
+
+## Examples
+
+Using a template can provide means to individually style the result output, for example a simple list can be turned into a phabricator task list.
+
+```
+{{#ask: [[Category:Task]]
+ ...
+ |outlineproperties=Severity
+ |template=phab-view
+}}
+```
+
+![image](https://user-images.githubusercontent.com/1245473/51059660-d2826b00-15e4-11e9-8ff3-bb1591b04e81.png)
+
+```
+{{#ask: [[Category:Task]]
+ ...
+ |outlineproperties=Severity, Assigned to
+ |template=phab-view
+}}
+```
+
+![image](https://user-images.githubusercontent.com/1245473/51059791-52103a00-15e5-11e9-85cf-86c503a10b55.png)
diff --git a/www/wiki/extensions/SemanticResultFormats/src/Outline/TemplateBuilder.php b/www/wiki/extensions/SemanticResultFormats/src/Outline/TemplateBuilder.php
new file mode 100644
index 00000000..e6982e29
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/Outline/TemplateBuilder.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace SRF\Outline;
+
+use SMW\Query\PrintRequest;
+use SRF\Outline\OutlineTree;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.1
+ *
+ * @author mwjames
+ */
+class TemplateBuilder {
+
+ /**
+ * @var []
+ */
+ private $params = [];
+
+ /**
+ * @var Linker
+ */
+ private $linker;
+
+ /**
+ * @var string
+ */
+ private $template = '';
+
+ /**
+ * @param array $params
+ */
+ public function __construct( array $params ) {
+ $this->params = $params;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param Linker|null|false $linker
+ */
+ public function setLinker( $linker ) {
+ $this->linker = $linker;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param OutlineTree $tree
+ *
+ * @return string
+ */
+ public function build( OutlineTree $outlineTree ) {
+ $this->tree( $outlineTree );
+
+ return $this->template;
+ }
+
+ private function tree( $outlineTree, $level = 0 ) {
+
+ if ( $outlineTree->items !== null ) {
+ foreach ( $outlineTree->items as $i => $item ) {
+ $this->template .= $this->item( $i, $item );
+ }
+ }
+
+ foreach ( $outlineTree->tree as $key => $node ) {
+ $property = $this->params['outlineproperties'][$level];
+ $class = $this->params['template'] . '-section-' . strtolower( str_replace( ' ', '-', $property ) );
+
+ $this->template .= "<div class='$class'>";
+ $this->template .= $this->open( $this->params['template'] . "-header" );
+ $this->template .= $this->parameter( $property, $key );
+ $this->template .= $this->parameter( "#outlinelevel", $level );
+ $this->template .= $this->parameter( "#itemcount", $node->leafCount );
+ $this->template .= $this->parameter( "#userparam", $this->params['userparam'] );
+ $this->template .= $this->close();
+ $this->template .= "<div class='" . $this->params['template'] . "-items'>";
+ $this->tree( $node, $level + 1 );
+ $this->template .= "</div>";
+ $this->template .= "</div>";
+ }
+ }
+
+ private function item( $i, $item ) {
+
+ $first_col = true;
+ $template = '';
+ $linker = $this->params['link'] === 'all' ? $this->linker : null;
+ $itemnumber = 0;
+
+ foreach ( $item->row as $resultArray ) {
+
+ $printRequest = $resultArray->getPrintRequest();
+ $val = $printRequest->getText( SMW_OUTPUT_WIKI, null );
+
+ if ( in_array( $val, $this->params['outlineproperties'] ) ) {
+ continue;
+ }
+
+ while ( ( $dv = $resultArray->getNextDataValue() ) !== false ) {
+ $template .= $this->open( $this->params['template'] . '-item' );
+ $template .= $this->parameter( "#itemsection", $i );
+
+ $template .= $this->parameter( "#itemnumber", $itemnumber );
+ $template .= $this->parameter( "#userparam", $this->params['userparam'] );
+
+ $template .= $this->itemText( $dv, $linker, $printRequest, $first_col );
+ $template .= $this->close();
+
+ $itemnumber++;
+ }
+ }
+
+ return "<div class='" . $this->params['template'] . "-item'>" . $template . '</div>';
+ }
+
+ private function itemText( $dv, $linker, $printRequest, &$first_col ) {
+
+ if ( $first_col && $printRequest->isMode( PrintRequest::PRINT_THIS ) ) {
+ $first_col = false;
+
+ if ( $linker === null && ( $caption = $dv->getDisplayTitle() ) !== '' ) {
+ $dv->setCaption( $caption );
+ }
+
+ $text = $dv->getShortText(
+ SMW_OUTPUT_WIKI,
+ $this->params['link'] === 'subject' ? $this->linker : $linker
+ );
+
+ return $this->parameter( "#itemsubject", $text );
+ }
+
+ $text = $dv->getShortText(
+ SMW_OUTPUT_WIKI,
+ $linker
+ );
+
+ return $this->parameter( $printRequest->getLabel(), $text );
+ }
+
+ private function open( $v ) {
+ return "{{" . $v;
+ }
+
+ private function parameter( $k, $v ) {
+ return " |$k=$v";
+ }
+
+ private function close() {
+ return "}}";
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticResultFormats/src/ResourceFormatter.php b/www/wiki/extensions/SemanticResultFormats/src/ResourceFormatter.php
new file mode 100644
index 00000000..4f4044b0
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/ResourceFormatter.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace SRF;
+
+use Html;
+use SMWOutputs as ResourceManager;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @since 3.0
+ *
+ * @license GNU GPL v2 or later
+ * @author mwjames
+ */
+class ResourceFormatter {
+
+ /**
+ * @since 3.0
+ *
+ * @param array $modules
+ * @param array $styleModules
+ */
+ public static function registerResources( array $modules = [], array $styleModules = [] ) {
+
+ foreach ( $modules as $module ) {
+ ResourceManager::requireResource( $module );
+ }
+
+ foreach ( $styleModules as $styleModule ) {
+ ResourceManager::requireStyle( $styleModule );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function session() {
+ return 'smw-' . uniqid();
+ }
+
+ /**
+ * Convenience method generating a visual placeholder before any
+ * JS is registered to indicate that resources (JavaScript, CSS)
+ * are being loaded and once ready ensure to set
+ * ( '.smw-spinner' ).hide()
+ *
+ * @since 3.0
+ */
+ public static function placeholder() {
+ self::registerResources( [], [ 'ext.smw.style' ] );
+
+ return Html::rawElement(
+ 'div',
+ [ 'class' => 'srf-loading-dots' ]
+ );
+ }
+
+ /**
+ *
+ * @since 3.0
+ *
+ * @param string $id
+ * @param array $data
+ */
+ public static function encode( $id, $data ) {
+ ResourceManager::requireHeadItem(
+ $id,
+ \Skin::makeVariablesScript(
+ [
+ $id => json_encode( $data )
+ ],
+ false
+ )
+ );
+ }
+
+ /**
+ * @param QueryResult $queryResult
+ * @param $outputMode
+ *
+ * @return string
+ */
+ public static function getData( QueryResult $queryResult, $outputMode, $parameters = [] ) {
+
+ // Add parameters that are only known to the specific printer
+ $ask = $queryResult->getQuery()->toArray();
+
+ foreach ( $parameters as $key => $value ) {
+ if ( is_string( $value ) || is_integer( $value ) || is_bool( $value ) ) {
+ $ask['parameters'][$key] = $value;
+ }
+ }
+
+ // Combine all data into one object
+ $data = [
+ 'query' => [
+ 'result' => $queryResult->toArray(),
+ 'ask' => $ask
+ ]
+ ];
+
+ return $data;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/iCalendar/IcalTimezoneFormatter.php b/www/wiki/extensions/SemanticResultFormats/src/iCalendar/IcalTimezoneFormatter.php
new file mode 100644
index 00000000..5437452a
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/iCalendar/IcalTimezoneFormatter.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace SRF\iCalendar;
+
+use DateTimeZone;
+use Exception;
+
+/**
+ * Create the iCalendar's vTimezone component
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author HgO
+ */
+class IcalTimezoneFormatter {
+
+ /**
+ * @var array
+ */
+ private $localTimezones = [];
+
+ /**
+ * @var array
+ */
+ private $transitions = [];
+
+ /**
+ * @var array
+ */
+ private $offsets = [];
+
+ /**
+ * @since 3.0
+ */
+ public function __construct() {
+ $this->localTimezones = [ $GLOBALS['wgLocaltimezone'] ];
+ }
+
+ /**
+ * Set a list of local timezones.
+ *
+ * @since 3.0
+ *
+ * @param array|string $localTimezones
+ */
+ public function setLocalTimezones( $localTimezones ) {
+
+ if ( is_array( $localTimezones ) ) {
+ $localTimezones = $localTimezones;
+ } elseif ( strpos( $localTimezones, ',' ) !== false ) {
+ $localTimezones = explode( ',', $localTimezones );
+ } elseif ( $localTimezones !== '' ) {
+ $localTimezones = [ $localTimezones ];
+ } else {
+ $localTimezones = [];
+ }
+
+ $this->localTimezones = $localTimezones;
+ }
+
+ /**
+ * Calculate transitions for each timezone.
+ *
+ * @since 3.0
+ *
+ * @param integer $from Timestamp from which transitions are generated.
+ * @param integer $to Timestamp until which transitions are generated.
+ *
+ * @return boolean
+ */
+ public function calcTransitions( $from = null, $to = null ) {
+
+ if ( $this->localTimezones === [] ) {
+ return false;
+ }
+
+ if ( $from === null || $to === null ) {
+ return false;
+ }
+
+ foreach ( $this->localTimezones as $timezone ) {
+ try {
+ $dateTimezone = new DateTimeZone( $timezone );
+ }
+ catch ( Exception $e ) {
+ continue;
+ }
+
+ $transitions = $dateTimezone->getTransitions( $from, $to );
+
+ if ( $transitions === false ) {
+ continue;
+ }
+
+ $min = 0;
+ $max = 1;
+
+ foreach ( $transitions as $i => $transition ) {
+ if ( $transition['ts'] < $from ) {
+ $min = $i;
+ continue;
+ }
+
+ if ( $transition['ts'] > $to ) {
+ $max = $i;
+ break;
+ }
+ }
+
+ $this->offsets[$timezone] = $transitions[max( $min - 1, 0 )]['offset'];
+ $this->transitions[$timezone] = array_slice( $transitions, $min, $max - $min );
+ }
+
+ return true;
+ }
+
+ /**
+ * Generate the transitions for a given range, for each timezones, in the
+ * iCalendar format.
+ *
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getTransitions() {
+
+ $result = '';
+
+ if ( $this->transitions === null || $this->transitions === [] ) {
+ return $result;
+ }
+
+ foreach ( $this->transitions as $timezone => $transitions ) {
+ // cf. http://www.kanzaki.com/docs/ical/vtimezone.html
+ $result .= "BEGIN:VTIMEZONE\r\n";
+ $result .= "TZID:$timezone\r\n";
+
+ $tzfrom = $this->offsets[$timezone] / 3600;
+ foreach ( $transitions as $transition ) {
+ $dst = ( $transition['isdst'] ) ? "DAYLIGHT" : "STANDARD";
+ $result .= "BEGIN:$dst\r\n";
+
+ $start_date = date( 'Ymd\THis', $transition['ts'] );
+ $result .= "DTSTART:$start_date\r\n";
+
+ $offset = $transition['offset'] / 3600;
+
+ $offset_from = $this->formatTimezoneOffset( $tzfrom );
+ $result .= "TZOFFSETFROM:$offset_from\r\n";
+
+ $offset_to = $this->formatTimezoneOffset( $offset );
+ $result .= "TZOFFSETTO:$offset_to\r\n";
+
+ if ( !empty( $transition['abbr'] ) ) {
+ $result .= "TZNAME:{$transition['abbr']}\r\n";
+ }
+
+ $result .= "END:$dst\r\n";
+
+ $tzfrom = $offset;
+ }
+
+ $result .= "END:VTIMEZONE\r\n";
+ }
+
+ // Clear the calculation
+ $this->transitions = [];
+
+ return $result;
+ }
+
+ /**
+ * Format an integer offset to '+hhii', where hh are the hours, and ii the
+ * minutes
+ *
+ * @param int $offset
+ */
+ private function formatTimezoneOffset( $offset ) {
+ return sprintf( '%s%02d%02d', $offset >= 0 ? '+' : '-', abs( floor( $offset ) ), ( $offset - floor( $offset ) ) * 60 );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/iCalendar/iCalendarFileExportPrinter.php b/www/wiki/extensions/SemanticResultFormats/src/iCalendar/iCalendarFileExportPrinter.php
new file mode 100644
index 00000000..e13e243a
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/iCalendar/iCalendarFileExportPrinter.php
@@ -0,0 +1,368 @@
+<?php
+
+namespace SRF\iCalendar;
+
+use SMW\Query\Result\ResultArray;
+use SMWDataValueFactory as DataValueFactory;
+use SMWExportPrinter as FileExportPrinter;
+use SMWQuery as Query;
+use SMWQueryProcessor as QueryProcessor;
+use SMWQueryResult as QueryResult;
+use SMWTimeValue as TimeValue;
+use WikiPage;
+
+/**
+ * Printer class for iCalendar exports
+ *
+ * @see https://en.wikipedia.org/wiki/ICalendar
+ * @see https://tools.ietf.org/html/rfc5545
+ *
+ * @license GNU GPL v2+
+ * @since 1.5
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author Jeroen De Dauw
+ */
+class iCalendarFileExportPrinter extends FileExportPrinter {
+
+ /**
+ * @var string
+ */
+ private $title;
+
+ /**
+ * @var string
+ */
+ private $description;
+
+ /**
+ * @var IcalTimezoneFormatter
+ */
+ private $icalTimezoneFormatter;
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return wfMessage( 'srf_printername_icalendar' )->text();
+ }
+
+ /**
+ * @see FileExportPrinter::getMimeType
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getMimeType( QueryResult $queryResult ) {
+ return 'text/calendar';
+ }
+
+ /**
+ * @see FileExportPrinter::getFileName
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFileName( QueryResult $queryResult ) {
+
+ if ( $this->title != '' ) {
+ return str_replace( ' ', '_', $this->title ) . '.ics';
+ }
+
+ return 'iCalendar.ics';
+ }
+
+ /**
+ * @see FileExportPrinter::getQueryMode
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getQueryMode( $context ) {
+ return ( $context == QueryProcessor::SPECIAL_PAGE ) ? Query::MODE_INSTANCES : Query::MODE_NONE;
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['title'] = [
+ 'default' => '',
+ 'message' => 'srf_paramdesc_icalendartitle',
+ ];
+
+ $params['description'] = [
+ 'default' => '',
+ 'message' => 'srf_paramdesc_icalendardescription',
+ ];
+
+ $params['timezone'] = [
+ 'default' => '',
+ 'message' => 'srf-paramdesc-icalendar-timezone',
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @see ResultPrinter::handleParameters
+ *
+ * {@inheritDoc}
+ */
+ protected function handleParameters( array $params, $outputMode ) {
+ parent::handleParameters( $params, $outputMode );
+
+ $this->title = trim( $params['title'] );
+ $this->description = trim( $params['description'] );
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ if ( $outputMode == SMW_OUTPUT_FILE ) {
+ return $this->getIcal( $res );
+ }
+
+ return $this->getIcalLink( $res, $outputMode );
+ }
+
+ /**
+ * Returns the query result in iCal.
+ */
+ private function getIcal( QueryResult $res ) {
+
+ $this->icalTimezoneFormatter = new IcalTimezoneFormatter();
+
+ $this->icalTimezoneFormatter->setLocalTimezones(
+ isset( $this->params['timezone'] ) ? $this->params['timezone'] : []
+ );
+
+ $result = '';
+
+ if ( $this->title == '' ) {
+ $this->title = $GLOBALS['wgSitename'];
+ }
+
+ $result .= "BEGIN:VCALENDAR\r\n";
+ $result .= "PRODID:-//SMW Project//Semantic Result Formats\r\n";
+ $result .= "VERSION:2.0\r\n";
+ $result .= "METHOD:PUBLISH\r\n";
+ $result .= "X-WR-CALNAME:" . $this->title . "\r\n";
+
+ if ( $this->description !== '' ) {
+ $result .= "X-WR-CALDESC:" . $this->description . "\r\n";
+ }
+
+ $events = '';
+
+ while ( $row = $res->getNext() ) {
+ $events .= $this->getIcalForItem( $row );
+ }
+
+ $result .= $this->icalTimezoneFormatter->getTransitions();
+ $result .= $events;
+ $result .= "END:VCALENDAR\r\n";
+
+ return $result;
+ }
+
+ /**
+ * Returns html for a link to a query that returns the iCal.
+ */
+ private function getIcalLink( QueryResult $res, $outputMode ) {
+
+ if ( $this->getSearchLabel( $outputMode ) ) {
+ $label = $this->getSearchLabel( $outputMode );
+ } else {
+ $label = wfMessage( 'srf_icalendar_link' )->inContentLanguage()->text();
+ }
+
+ $link = $res->getQueryLink( $label );
+ $link->setParameter( 'icalendar', 'format' );
+
+ if ( $this->title !== '' ) {
+ $link->setParameter( $this->title, 'title' );
+ }
+
+ if ( $this->description !== '' ) {
+ $link->setParameter( $this->description, 'description' );
+ }
+
+ if ( array_key_exists( 'limit', $this->params ) ) {
+ $link->setParameter( $this->params['limit'], 'limit' );
+ } else { // use a reasonable default limit
+ $link->setParameter( 20, 'limit' );
+ }
+
+ // yes, our code can be viewed as HTML if requested, no more parsing needed
+ $this->isHTML = ( $outputMode == SMW_OUTPUT_HTML );
+
+ return $link->getText( $outputMode, $this->mLinker );
+ }
+
+ /**
+ * Returns the iCal for a single item.
+ *
+ * @param ResultArray[] $row
+ *
+ * @return string
+ * @throws \MWException
+ */
+ private function getIcalForItem( array $row ) {
+ $result = '';
+
+ $subjectDI = $row[0]->getResultSubject(); // get the object
+ $subjectDV = DataValueFactory::getInstance()->newDataValueByItem( $subjectDI, null );
+
+ $params = [
+ 'summary' => $subjectDV->getShortWikiText()
+ ];
+
+ $from = null;
+ $to = null;
+ foreach ( $row as /* SMWResultArray */
+ $field ) {
+ // later we may add more things like a generic
+ // mechanism to add whatever you want :)
+ // could include funny things like geo, description etc. though
+ $req = $field->getPrintRequest();
+ $label = strtolower( $req->getLabel() );
+
+ switch ( $label ) {
+ case 'start':
+ case 'end':
+ if ( $req->getTypeID() == '_dat' ) {
+ $dataValue = $field->getNextDataValue();
+
+ if ( $dataValue === false ) {
+ unset( $params[$label] );
+ } else {
+ $params[$label] = $this->parsedate( $dataValue, $label == 'end' );
+
+ $timestamp = strtotime( $params[$label] );
+ if ( $from === null || $timestamp < $from ) {
+ $from = $timestamp;
+ }
+ if ( $to === null || $timestamp > $to ) {
+ $to = $timestamp;
+ }
+ }
+ }
+ break;
+ case 'location':
+ case 'description':
+ case 'summary':
+ $value = $field->getNextDataValue();
+ if ( $value !== false ) {
+ $params[$label] = $value->getShortWikiText();
+ }
+ break;
+ }
+ }
+
+ $this->icalTimezoneFormatter->calcTransitions( $from, $to );
+
+ $title = $subjectDI->getTitle();
+ $timestamp = WikiPage::factory( $title )->getTimestamp();
+ $url = $title->getFullURL();
+
+ $result .= "BEGIN:VEVENT\r\n";
+ $result .= "SUMMARY:" . $this->escape( $params['summary'] ) . "\r\n";
+ $result .= "URL:$url\r\n";
+ $result .= "UID:$url\r\n";
+
+ if ( array_key_exists( 'start', $params ) ) {
+ $result .= "DTSTART:" . $params['start'] . "\r\n";
+ }
+
+ if ( array_key_exists( 'end', $params ) ) {
+ $result .= "DTEND:" . $params['end'] . "\r\n";
+ }
+
+ if ( array_key_exists( 'location', $params ) ) {
+ $result .= "LOCATION:" . $this->escape( $params['location'] ) . "\r\n";
+ }
+
+ if ( array_key_exists( 'description', $params ) ) {
+ $result .= "DESCRIPTION:" . $this->escape( $params['description'] ) . "\r\n";
+ }
+
+ $t = strtotime( str_replace( 'T', ' ', $timestamp ) );
+ $result .= "DTSTAMP:" . date( "Ymd", $t ) . "T" . date( "His", $t ) . "\r\n";
+ $result .= "SEQUENCE:" . $title->getLatestRevID() . "\r\n";
+ $result .= "END:VEVENT\r\n";
+
+ return $result;
+ }
+
+ /**
+ * Extract a date string formatted for iCalendar from a SMWTimeValue object.
+ */
+ private function parsedate( TimeValue $dv, $isend = false ) {
+ $year = $dv->getYear();
+
+ // ISO range is limited to four digits
+ if ( ( $year > 9999 ) || ( $year < -9998 ) ) {
+ return '';
+ }
+
+ $year = number_format( $year, 0, '.', '' );
+ $time = str_replace( ':', '', $dv->getTimeString( false ) );
+
+ // increment by one day, compute date to cover leap years etc.
+ if ( ( $time == false ) && ( $isend ) ) {
+ $dv = DataValueFactoryg::getInstance()->newDataValueByType(
+ '_dat',
+ $dv->getWikiValue() . 'T00:00:00-24:00'
+ );
+ }
+
+ $month = $dv->getMonth();
+
+ if ( strlen( $month ) == 1 ) {
+ $month = '0' . $month;
+ }
+
+ $day = $dv->getDay();
+
+ if ( strlen( $day ) == 1 ) {
+ $day = '0' . $day;
+ }
+
+ $result = $year . $month . $day;
+
+ if ( $time != false ) {
+ $result .= "T$time";
+ }
+
+ return $result;
+ }
+
+ /**
+ * Implements esaping of special characters for iCalendar properties of type
+ * TEXT. This is defined in RFC2445 Section 4.3.11.
+ */
+ private function escape( $text ) {
+ // Note that \\ is a PHP escaped single \ here
+ return str_replace( [ "\\", "\n", ";", "," ], [ "\\\\", "\\n", "\\;", "\\," ], $text );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/vCard/Address.php b/www/wiki/extensions/SemanticResultFormats/src/vCard/Address.php
new file mode 100644
index 00000000..9145bb21
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/vCard/Address.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace SRF\vCard;
+
+/**
+ * Represents a single address entry in an vCard
+ *
+ * @see http://www.semantic-mediawiki.org/wiki/vCard
+ *
+ * @license GNU GPL v2+
+ * @since 1.5
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author Frank Dengler
+ */
+class Address {
+
+ /**
+ * @var string
+ */
+ private $type;
+
+ /**
+ * @var array
+ */
+ private $adr = [];
+
+ /**
+ * @param string $type
+ * @param array $adr
+ */
+ public function __construct( $type, array $adr = [] ) {
+ $this->type = $type;
+ $this->adr = $adr;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @return boolean
+ */
+ public function hasAddress() {
+ return $this->adr !== [];
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function set( $key, $value ) {
+ $this->adr[$key] = $value;
+ }
+
+ /**
+ * @return string
+ */
+ public function text() {
+
+ if ( $this->type == "" ) {
+ $this->type = "WORK";
+ }
+
+ $adr = [];
+
+ // Expected sequence as defined by
+ // https://tools.ietf.org/html/rfc6350#section-6.3.1
+ $map = [
+ 'pobox',
+ 'ext',
+ 'street',
+ 'locality',
+ 'region',
+ 'code',
+ 'country'
+ ];
+
+ foreach ( $map as $k ) {
+ $adr[] = isset( $this->adr[$k] ) ? vCard::escape( $this->adr[$k] ) : '';
+ }
+
+ return "ADR;TYPE=$this->type;CHARSET=UTF-8:" . implode( ';', $adr ) . "\r\n";
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/vCard/Email.php b/www/wiki/extensions/SemanticResultFormats/src/vCard/Email.php
new file mode 100644
index 00000000..908c6cf9
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/vCard/Email.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace SRF\vCard;
+
+/**
+ * Represents a single email entry in an vCard entry.
+ *
+ * @see http://www.semantic-mediawiki.org/wiki/vCard
+ *
+ * @license GNU GPL v2+
+ * @since 1.5
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author Frank Dengler
+ */
+class Email {
+
+ /**
+ * @var string
+ */
+ private $type;
+
+ /**
+ * @var string
+ */
+ private $emailaddress;
+
+ /**
+ * @param string $type
+ * @param string $emailaddress
+ */
+ public function __construct( $type, $emailaddress ) {
+ $this->type = $type;
+ $this->emailaddress = $emailaddress; // no escape, normally not needed anyway
+ }
+
+ /**
+ * Creates the vCard output for a single email item.
+ */
+ public function text() {
+
+ if ( $this->type == "" ) {
+ $this->type = "INTERNET";
+ }
+
+ return "EMAIL;TYPE=$this->type:$this->emailaddress\r\n";
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticResultFormats/src/vCard/Tel.php b/www/wiki/extensions/SemanticResultFormats/src/vCard/Tel.php
new file mode 100644
index 00000000..15d84c92
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/vCard/Tel.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace SRF\vCard;
+
+/**
+ * Represents a single telephone entry in an vCard entry.
+ *
+ * @see http://www.semantic-mediawiki.org/wiki/vCard
+ *
+ * @license GNU GPL v2+
+ * @since 1.5
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author Frank Dengler
+ */
+class Tel {
+
+ /**
+ * @var string
+ */
+ private $type;
+
+ /**
+ * @var string
+ */
+ private $telnumber;
+
+ /**
+ * @param string $type
+ * @param string $telnumber
+ */
+ public function __construct( $type, $telnumber ) {
+ $this->type = $type; // may be a vCard value list using ",", no escaping
+ $this->telnumber = vCard::escape( $telnumber ); // escape to be sure
+ }
+
+ /**
+ * Creates the vCard output for a single telephone item.
+ */
+ public function text() {
+
+ if ( $this->type == "" ) {
+ $this->type = "WORK";
+ }
+
+ return "TEL;TYPE=$this->type:$this->telnumber\r\n";
+ }
+
+}
+
diff --git a/www/wiki/extensions/SemanticResultFormats/src/vCard/vCard.php b/www/wiki/extensions/SemanticResultFormats/src/vCard/vCard.php
new file mode 100644
index 00000000..e5d6e0f9
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/vCard/vCard.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace SRF\vCard;
+
+use Article;
+use Title;
+
+/**
+ * Represents a single entry in an vCard
+ *
+ * @see http://www.semantic-mediawiki.org/wiki/vCard
+ *
+ * @license GNU GPL v2+
+ * @since 1.5
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author Frank Dengler
+ */
+class vCard {
+
+ /**
+ * @var string
+ */
+ private $uri;
+
+ /**
+ * @var string
+ */
+ private $text;
+
+ /**
+ * @var array
+ */
+ private $vcard = [];
+
+ /**
+ * @var boolean
+ */
+ private $isPublic = true;
+
+ /**
+ * @var integer
+ */
+ private $timestamp;
+
+ /**
+ * @since 3.0
+ *
+ * @param string $uri
+ * @param string $text
+ * @param array $vcard
+ */
+ public function __construct( $uri, $text, array $vcard ) {
+ $this->uri = $uri;
+ $this->text = $text;
+
+ $default = [
+ 'prefix' => '',
+ 'firstname' => '',
+ 'lastname' => '',
+ 'additionalname' => '',
+ 'suffix' => '',
+ 'fullname' => '',
+ 'tel' => [],
+ 'address' => [],
+ 'email' => [],
+ 'birthday' => '',
+ 'title' => '',
+ 'role' => '',
+ 'organization' => '',
+ 'department' => '',
+ 'category' => '',
+ 'url' => '',
+ 'note' => ''
+ ];
+
+ $this->vcard = $vcard + $default;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function set( $key, $value ) {
+ $this->vcard[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isPublic
+ */
+ public function isPublic( $isPublic ) {
+ $this->isPublic = $isPublic;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $timestamp
+ */
+ public function setTimestamp( $timestamp ) {
+ $this->timestamp = $timestamp;
+ }
+
+ /**
+ * Creates the vCard output for a single item.
+ *
+ * @return string
+ */
+ public function text() {
+
+ $vcard = $this->prepareCard( $this->vcard );
+
+ $text = "BEGIN:VCARD\r\n";
+ $text .= "VERSION:3.0\r\n";
+
+ // N and FN are required properties in vCard 3.0, we need to write something there
+ $text .= "N;CHARSET=UTF-8:" .
+ $vcard['lastname'] . ';' .
+ $vcard['firstname'] . ';' .
+ $vcard['additionalname'] . ';' .
+ $vcard['prefix'] . ';' .
+ $vcard['suffix'] . "\r\n";
+
+ $text .= "FN;CHARSET=UTF-8:" .
+ $vcard['label'] . "\r\n";
+
+ $text .= ( $this->isPublic ? 'CLASS:PUBLIC' : 'CLASS:CONFIDENTIAL' ) . "\r\n";
+
+ if ( $vcard['birthday'] !== "" ) {
+ $text .= "BDAY:" . $vcard['birthday'] . "\r\n";
+ }
+
+ if ( $vcard['title'] !== "" ) {
+ $text .= "TITLE;CHARSET=UTF-8:" . $vcard['title'] . "\r\n";
+ }
+
+ if ( $vcard['role'] !== "" ) {
+ $text .= "ROLE;CHARSET=UTF-8:" . $vcard['role'] . "\r\n";
+ }
+
+ if ( $vcard['organization'] !== "" ) {
+ $text .= "ORG;CHARSET=UTF-8:" . $vcard['organization'] . ';' . $vcard['department'] . "\r\n";
+ }
+
+ if ( $vcard['category'] !== "" ) {
+ $text .= "CATEGORIES;CHARSET=UTF-8:" . $vcard['category'] . "\r\n";
+ }
+
+ foreach ( $vcard['email'] as $e ) {
+ $text .= $e->text();
+ }
+
+ foreach ( $vcard['address'] as $a ) {
+ if ( $a->hasAddress() ) {
+ $text .= $a->text();
+ }
+ }
+
+ foreach ( $vcard['tel'] as $t ) {
+ $text .= $t->text();
+ }
+
+ if ( $vcard['note'] !== "" ) {
+ $text .= "NOTE;CHARSET=UTF-8:" . $vcard['note'] . "\r\n";
+ }
+
+ $text .= "SOURCE;CHARSET=UTF-8:$this->uri\r\n";
+
+ // The identifier for the product that created the vCard object
+ $text .= "PRODID:-////Semantic MediaWiki\r\n";
+
+ // A timestamp for the last time the vCard was updated
+ $text .= "REV:$this->timestamp\r\n";
+
+ // A URL pointing to a website that represents the person in some way
+ $text .= "URL:" . ( $vcard['url'] !== "" ? $vcard['url'] : $this->uri ) . "\r\n";
+
+ // Specifies a value that represents a persistent, globally unique
+ // identifier associated with the object.
+ $text .= "UID:$this->uri\r\n";
+ $text .= "END:VCARD\r\n";
+
+ return $text;
+ }
+
+ public static function escape( $text ) {
+ return str_replace( [ '\\', ',', ':', ';' ], [ '\\\\', '\,', '\:', '\;' ], $text );
+ }
+
+ private function prepareCard( $vcard ) {
+
+ $vcard['label'] = '';
+
+ $additionalname = $vcard['additionalname'];
+
+ // Read fullname or guess it in a simple way from other names that are
+ // given
+ if ( $vcard['fullname'] != '' ) {
+ $vcard['label'] = $vcard['fullname'];
+ } elseif ( $vcard['firstname'] . $vcard['lastname'] != '' ) {
+ $vcard['label'] = $vcard['firstname'] . ( ( ( $vcard['firstname'] != '' ) && ( $vcard['lastname'] != '' ) ) ? ' ' : '' ) . $vcard['lastname'];
+ } else {
+ $vcard['label'] = $this->text;
+ }
+
+ $vcard['label'] = self::escape( $vcard['label'] );
+
+ // read firstname and lastname, or guess it from other names that are given
+ if ( $vcard['firstname'] . $vcard['lastname'] == '' ) { // guessing needed
+ $nameparts = explode( ' ', $vcard['label'] );
+ // Accepted forms for guessing:
+ // "Lastname"
+ // "Firstname Lastname"
+ // "Firstname <Additionalnames> Lastname"
+ $vcard['lastname'] = self::escape( array_pop( $nameparts ) );
+
+ if ( count( $nameparts ) > 0 ) {
+ $vcard['firstname'] = self::escape( array_shift( $nameparts ) );
+ }
+
+ foreach ( $nameparts as $name ) {
+ $vcard['additionalname'] .= ( $vcard['additionalname'] != '' ? ',' : '' ) . self::escape( $name );
+ }
+ } else {
+ $vcard['firstname'] = self::escape( $vcard['firstname'] );
+ $vcard['lastname'] = self::escape( $vcard['lastname'] );
+ }
+
+ // no escape, can be a value list
+ if ( $additionalname != '' ) {
+ $vcard['additionalname'] = $additionalname;
+ }
+
+ $vcard['prefix'] = self::escape( $vcard['prefix'] );
+ $vcard['suffix'] = self::escape( $vcard['suffix'] );
+ $vcard['title'] = self::escape( $vcard['title'] );
+ $vcard['role'] = self::escape( $vcard['role'] );
+ $vcard['organization'] = self::escape( $vcard['organization'] );
+ $vcard['department'] = self::escape( $vcard['department'] );
+ $vcard['note'] = self::escape( $vcard['note'] );
+
+ return $vcard;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticResultFormats/src/vCard/vCardFileExportPrinter.php b/www/wiki/extensions/SemanticResultFormats/src/vCard/vCardFileExportPrinter.php
new file mode 100644
index 00000000..6ca75fad
--- /dev/null
+++ b/www/wiki/extensions/SemanticResultFormats/src/vCard/vCardFileExportPrinter.php
@@ -0,0 +1,433 @@
+<?php
+
+namespace SRF\vCard;
+
+use SMWExportPrinter as FileExportPrinter;
+use SMWQuery as Query;
+use SMWQueryProcessor as QueryProcessor;
+use SMWQueryResult as QueryResult;
+use SMWTimeValue as TimeValue;
+use WikiPage;
+
+/**
+ * Printer class for creating vCard exports
+ *
+ * @see http://www.semantic-mediawiki.org/wiki/vCard
+ * @see https://tools.ietf.org/html/rfc6350
+ * @see https://www.w3.org/2002/12/cal/vcard-notes.html
+ *
+ * @license GNU GPL v2+
+ * @since 1.5
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author Frank Dengler
+ * @author mwjames
+ */
+class vCardFileExportPrinter extends FileExportPrinter {
+
+ /**
+ * @see FileExportPrinter::getName
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return wfMessage( 'srf_printername_vcard' )->text();
+ }
+
+ /**
+ * @see FileExportPrinter::getMimeType
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getMimeType( QueryResult $queryResult ) {
+ return 'text/x-vcard';
+ }
+
+ /**
+ * @see FileExportPrinter::getFileName
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFileName( QueryResult $queryResult ) {
+
+ if ( $this->params['filename'] !== '' ) {
+
+ if ( strpos( $this->params['filename'], '.vcf' ) === false ) {
+ $this->params['filename'] .= '.vcf';
+ }
+
+ return str_replace( ' ', '_', $this->params['filename'] );
+ } elseif ( $this->getSearchLabel( SMW_OUTPUT_WIKI ) != '' ) {
+ return str_replace( ' ', '_', $this->getSearchLabel( SMW_OUTPUT_WIKI ) ) . '.vcf';
+ }
+
+ return 'vCard.vcf';
+ }
+
+ /**
+ * @see FileExportPrinter::getQueryMode
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getQueryMode( $context ) {
+ return ( $context == QueryProcessor::SPECIAL_PAGE ) ? Query::MODE_INSTANCES : Query::MODE_NONE;
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['filename'] = [
+ 'message' => 'smw-paramdesc-filename',
+ 'default' => 'vCard.vcf',
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ // Always return a link for when the output mode is not a file request,
+ // a file request is normally only initiated when resolving the query
+ // via Special:Ask
+ if ( $outputMode !== SMW_OUTPUT_FILE ) {
+ return $this->getVCardLink( $res, $outputMode );
+ }
+
+ return $this->getVCardContent( $res );
+ }
+
+ private function getVCardLink( QueryResult $res, $outputMode ) {
+
+ // Can be viewed as HTML if requested, no more parsing needed
+ $this->isHTML = $outputMode == SMW_OUTPUT_HTML;
+
+ if ( $this->getSearchLabel( $outputMode ) ) {
+ $label = $this->getSearchLabel( $outputMode );
+ } else {
+ $label = wfMessage( 'srf_vcard_link' )->inContentLanguage()->text();
+ }
+
+ $link = $res->getQueryLink( $label );
+ $link->setParameter( 'vcard', 'format' );
+
+ if ( $this->getSearchLabel( SMW_OUTPUT_WIKI ) != '' ) {
+ $link->setParameter( $this->getSearchLabel( SMW_OUTPUT_WIKI ), 'searchlabel' );
+ }
+
+ if ( array_key_exists( 'limit', $this->params ) ) {
+ $link->setParameter( $this->params['limit'], 'limit' );
+ } else { // use a reasonable default limit
+ $link->setParameter( 20, 'limit' );
+ }
+
+ return $link->getText( $outputMode, $this->mLinker );
+ }
+
+ /**
+ * @param QueryResult $res
+ *
+ * @return string
+ * @throws \MWException
+ */
+ private function getVCardContent( $res ) {
+
+ $result = '';
+ $vCards = [];
+
+ $row = $res->getNext();
+ $isPublic = $this->isPublic();
+
+ while ( $row !== false ) {
+ // Subject of the Result
+ $subject = $row[0]->getResultSubject();
+ $title = $subject->getTitle();
+
+ // Specifies a value that represents a persistent, globally unique
+ // identifier associated with the object.
+ $uri = $title->getFullURL();
+
+ // A timestamp for the last time the vCard was updated
+ $timestamp = WikiPage::factory( $title )->getTimestamp();
+ $text = $title->getText();
+
+ $vCards[] = $this->newVCard( $row, $uri, $text, $timestamp, $isPublic );
+ $row = $res->getNext();
+ }
+
+ foreach ( $vCards as $vCard ) {
+ $result .= $vCard->text();
+ }
+
+ return $result;
+ }
+
+ private function newVCard( $row, $uri, $text, $timestamp, $isPublic ) {
+
+ $vCard = new vCard(
+ $uri,
+ $text,
+ [
+
+ // something like 'Dr.'
+ 'prefix' => '',
+
+ // given name
+ 'firstname' => '',
+
+ // family name
+ 'lastname' => '',
+
+ // typically the "middle" name (second first name)
+ 'additionalname' => '',
+
+ // things like "jun." or "sen."
+ 'suffix' => '',
+
+ // the "formatted name", may be independent from
+ // first/lastname & co.
+ 'fullname' => '',
+
+ 'tel' => [],
+ 'address' => [],
+ 'email' => [],
+ // a date
+ 'birthday' => '',
+
+ // organisational details
+ 'organization' => '',
+ 'department' => '',
+ 'title' => '',
+ 'role' => '',
+ 'category' => '',
+
+ // homepage, a legal URL
+ 'url' => '',
+
+ // any text
+ 'note' => ''
+ ]
+ );
+
+ $tels = [];
+ $emails = [];
+
+ $addresses['work'] = new Address( 'WORK' );
+ $addresses['home'] = new Address( 'HOME' );
+
+ foreach ( $row as $field ) {
+ $this->mapField( $field, $vCard, $tels, $addresses, $emails );
+ }
+
+ $vCard->set( 'tel', $tels );
+ $vCard->set( 'address', $addresses );
+ $vCard->set( 'email', $emails );
+
+ $vCard->isPublic( $isPublic );
+ $vCard->setTimestamp( $timestamp );
+
+ return $vCard;
+ }
+
+ private function isPublic() {
+ // heuristic for setting confidentiality level of vCard:
+ global $wgGroupPermissions;
+
+ if ( ( array_key_exists( '*', $wgGroupPermissions ) ) && ( array_key_exists(
+ 'read',
+ $wgGroupPermissions['*']
+ ) ) ) {
+ return $wgGroupPermissions['*']['read'];
+ }
+
+ return true;
+ }
+
+ private function mapField( $field, $vCard, &$tels, &$addresses, &$emails ) {
+
+ $printRequest = $field->getPrintRequest();
+
+ switch ( strtolower( $printRequest->getLabel() ) ) {
+ case "name":
+ $vCard->set( 'fullname', $this->getFieldValue( $field ) );
+ break;
+ case "prefix":
+ $vCard->set( 'prefix', $this->getFieldCommaList( $field ) );
+ break;
+ case "suffix":
+ $vCard->set( 'suffix', $this->getFieldCommaList( $field ) );
+ break;
+ case "firstname":
+ // save only the first
+ $vCard->set( 'firstname', $this->getFieldValue( $field ) );
+ break;
+ case "additionalname":
+ case "extraname":
+ $vCard->set( 'additionalname', $this->getFieldCommaList( $field ) );
+ break;
+ case "lastname":
+ // save only the first
+ $vCard->set( 'lastname', $this->getFieldValue( $field ) );
+ break;
+ case "note":
+ $vCard->set( 'note', $this->getFieldCommaList( $field ) );
+ break;
+ case "category":
+ $vCard->set( 'category', $this->getFieldCommaList( $field ) );
+ case "email":
+ while ( $value = $field->getNextDataValue() ) {
+ $emails[] = new Email( 'INTERNET', $value->getShortWikiText() );
+ }
+ break;
+ case "workphone":
+ while ( $value = $field->getNextDataValue() ) {
+ $tels[] = new Tel( 'WORK', $value->getShortWikiText() );
+ }
+ break;
+ case "cellphone":
+ while ( $value = $field->getNextDataValue() ) {
+ $tels[] = new Tel( 'CELL', $value->getShortWikiText() );
+ }
+ break;
+ case "homephone":
+ while ( $value = $field->getNextDataValue() ) {
+ $tels[] = new Tel( 'HOME', $value->getShortWikiText() );
+ }
+ break;
+ case "organization":
+ $vCard->set( 'organization', $this->getFieldValue( $field ) );
+ break;
+ case "workpostofficebox":
+ if ( ( $workpostofficebox = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['work']->set( 'pobox', $workpostofficebox );
+ }
+ break;
+ case "workextendedaddress":
+ if ( ( $workextendedaddress = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['work']->set( 'ext', $workextendedaddress );
+ }
+ break;
+ case "workstreet":
+ if ( ( $workstreet = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['work']->set( 'street', $workstreet );
+ }
+ break;
+ case "worklocality":
+ if ( ( $worklocality = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['work']->set( 'locality', $worklocality );
+ }
+ break;
+ case "workregion":
+ if ( ( $workregion = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['work']->set( 'region', $workregion );
+ }
+ break;
+ case "workpostalcode":
+ if ( ( $workpostalcode = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['work']->set( 'code', $workpostalcode );
+ }
+ break;
+ case "workcountry":
+ if ( ( $workcountry = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['work']->set( 'country', $workcountry );
+ }
+ break;
+ case "homepostofficebox":
+ if ( ( $homepostofficebox = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['home']->set( 'pobox', $homepostofficebox );
+ }
+ break;
+ case "homeextendedaddress":
+ if ( ( $homeextendedaddress = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['home']->set( 'ext', $homeextendedaddress );
+ }
+ break;
+ case "homestreet":
+ if ( ( $homestreet = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['home']->set( 'street', $homestreet );
+ }
+ break;
+ case "homelocality":
+ if ( ( $homelocality = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['home']->set( 'locality', $homelocality );
+ }
+ break;
+ case "homeregion":
+ if ( ( $homeregion = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['home']->set( 'region', $homeregion );
+ }
+ break;
+ case "homepostalcode":
+ if ( ( $homepostalcode = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['home']->set( 'code', $homepostalcode );
+ }
+ break;
+ case "homecountry":
+ if ( ( $homecountry = $this->getFieldValue( $field ) ) !== '' ) {
+ $addresses['home']->set( 'country', $homecountry );
+ }
+ break;
+ case "birthday":
+ if ( $printRequest->getTypeID() == TimeValue::TYPE_ID ) {
+ $value = $field->getNextDataValue();
+
+ if ( $value !== false ) {
+ $birthday = $value->getISO8601Date();
+ $vCard->set( 'birthday', $birthday );
+ }
+ }
+ break;
+ case "homepage":
+ if ( $printRequest->getTypeID() == "_uri" ) {
+ $value = $field->getNextDataValue();
+
+ if ( $value !== false ) {
+ $url = $value->getWikiValue();
+ $vCard->set( 'url', $url );
+ }
+ }
+ break;
+ }
+ }
+
+ private function getFieldCommaList( $field ) {
+
+ $list = '';
+
+ while ( $value = $field->getNextDataValue() ) {
+ $list .= ( $list ? ',' : '' ) . $value->getShortWikiText();
+ }
+
+ return $list;
+ }
+
+ private function getFieldValue( $field ) {
+
+ $v = '';
+
+ if ( ( $value = $field->getNextDataValue() ) !== false ) {
+ $v = $value->getShortWikiText();
+ }
+
+ return $v;
+ }
+
+}