mTemplateName = $templateName; $this->mTemplateFields = $templateFields; } public static function newFromName( $templateName ) { $template = new PFTemplate( $templateName, array() ); $template->loadTemplateFields(); return $template; } /** * @TODO - fix so that this function only gets called once per * template; right now it seems to get called once per field. (!) */ function loadTemplateFields() { $templateTitle = Title::makeTitleSafe( NS_TEMPLATE, $this->mTemplateName ); if ( !isset( $templateTitle ) ) { return; } $templateText = PFUtils::getPageText( $templateTitle ); // Ignore 'noinclude' sections and 'includeonly' tags. $templateText = StringUtils::delimiterReplace( '', '', '', $templateText ); $this->mTemplateText = strtr( $templateText, array( '' => '', '' => '' ) ); // The Cargo-based function is more specific; it only gets // data structure information from the template schema. If // there's no Cargo schema for this template, we call // loadTemplateFieldsSMWAndOther(), which doesn't require the // presence of SMW and can get non-SMW information as well. if ( defined( 'CARGO_VERSION' ) ) { $this->loadTemplateFieldsCargo( $templateTitle ); if ( count( $this->mTemplateFields ) > 0 ) { return; } } $this->loadTemplateFieldsSMWAndOther(); } /** * Get the fields of the template, along with the semantic property * attached to each one (if any), by parsing the text of the template. */ function loadTemplateFieldsSMWAndOther() { global $wgContLang; $templateFields = array(); $fieldNamesArray = array(); // The way this works is that fields are found and then stored // in an array based on their location in the template text, so // that they can be returned in the order in which they appear // in the template, not the order in which they were found. // Some fields can be found more than once (especially if // they're part of an "#if" statement), so they're only // recorded the first time they're found. // Replace all calls to #set within #arraymap with standard // SMW tags. This is done so that they will later get // parsed correctly. // This is "cheating", since it modifies the template text // (the rest of the function doesn't do that), but trying to // get the #arraymap check regexp to find both kinds of SMW // property tags seemed too hard to do. $this->mTemplateText = preg_replace( '/#arraymap.*{{\s*#set:\s*([^=]*)=([^}]*)}}/', '[[$1:$2]]', $this->mTemplateText ); // Look for "arraymap" parser function calls that map a // property onto a list. if ( $ret = preg_match_all( '/{{#arraymap:{{{([^|}]*:?[^|}]*)[^\[]*\[\[([^:]*:?[^:]*)::/mis', $this->mTemplateText, $matches ) ) { foreach ( $matches[1] as $i => $field_name ) { if ( ! in_array( $field_name, $fieldNamesArray ) ) { $propertyName = $matches[2][$i]; $this->loadPropertySettingInTemplate( $field_name, $propertyName, true ); $fieldNamesArray[] = $field_name; } } } elseif ( $ret === false ) { // There was an error in the preg_match_all() // call - let the user know about it. if ( preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR ) { print 'Page Forms error: backtrace limit exceeded during parsing! Please increase the value of pcre.backtrack_limit in php.ini or LocalSettings.php.'; } } // Look for normal property calls. if ( preg_match_all( '/\[\[([^:|\[\]]*:*?[^:|\[\]]*)::{{{([^\]\|}]*).*?\]\]/mis', $this->mTemplateText, $matches ) ) { foreach ( $matches[1] as $i => $propertyName ) { $field_name = trim( $matches[2][$i] ); if ( ! in_array( $field_name, $fieldNamesArray ) ) { $propertyName = trim( $propertyName ); $this->loadPropertySettingInTemplate( $field_name, $propertyName, false ); $fieldNamesArray[] = $field_name; } } } // Then, get calls to #set, #set_internal and #subobject. // (Thankfully, they all have similar syntax). if ( preg_match_all( '/#(set|set_internal|subobject):(.*?}}})\s*}}/mis', $this->mTemplateText, $matches ) ) { foreach ( $matches[2] as $match ) { if ( preg_match_all( '/([^|{]*?)=\s*{{{([^|}]*)/mis', $match, $matches2 ) ) { foreach ( $matches2[1] as $i => $propertyName ) { $fieldName = trim( $matches2[2][$i] ); if ( ! in_array( $fieldName, $fieldNamesArray ) ) { $propertyName = trim( $propertyName ); $this->loadPropertySettingInTemplate( $fieldName, $propertyName, false ); $fieldNamesArray[] = $fieldName; } } } } } // Then, get calls to #declare. (This is really rather // optional, since no one seems to use #declare.) if ( preg_match_all( '/#declare:(.*?)}}/mis', $this->mTemplateText, $matches ) ) { foreach ( $matches[1] as $match ) { $setValues = explode( '|', $match ); foreach ( $setValues as $valuePair ) { $keyAndVal = explode( '=', $valuePair ); if ( count( $keyAndVal ) == 2 ) { $propertyName = trim( $keyAndVal[0] ); $fieldName = trim( $keyAndVal[1] ); if ( ! in_array( $fieldName, $fieldNamesArray ) ) { $this->loadPropertySettingInTemplate( $fieldName, $propertyName, false ); $fieldNamesArray[] = $fieldName; } } } } } // Finally, get any non-semantic fields defined. if ( preg_match_all( '/{{{([^|}]*)/mis', $this->mTemplateText, $matches ) ) { foreach ( $matches[1] as $fieldName ) { $fieldName = trim( $fieldName ); if ( !empty( $fieldName ) && ( ! in_array( $fieldName, $fieldNamesArray ) ) ) { $cur_pos = stripos( $this->mTemplateText, $fieldName ); $this->mTemplateFields[$cur_pos] = PFTemplateField::create( $fieldName, $wgContLang->ucfirst( $fieldName ) ); $fieldNamesArray[] = $fieldName; } } } ksort( $this->mTemplateFields ); } /** * For a field name and its attached property name located in the * template text, create an PFTemplateField object out of it, and * add it to $this->mTemplateFields. * @param string $fieldName * @param string $propertyName * @param bool $isList */ function loadPropertySettingInTemplate( $fieldName, $propertyName, $isList ) { global $wgContLang; $templateField = PFTemplateField::create( $fieldName, $wgContLang->ucfirst( $fieldName ), $propertyName, $isList ); $cur_pos = stripos( $this->mTemplateText, $fieldName . '|' ); $this->mTemplateFields[$cur_pos] = $templateField; } function loadTemplateFieldsCargo( $templateTitle ) { $cargoFieldsOfTemplateParams = array(); // First, get the table name, and fields, declared for this // template. $templatePageID = $templateTitle->getArticleID(); $tableSchemaString = CargoUtils::getPageProp( $templatePageID, 'CargoFields' ); // See if there even is DB storage for this template - if not, // exit. if ( is_null( $tableSchemaString ) ) { // There's no declared table - but see if there's an // attached table. list( $tableName, $isDeclared ) = CargoUtils::getTableNameForTemplate( $templateTitle ); if ( $tableName == null ) { return null; } $mainTemplatePageID = CargoUtils::getTemplateIDForDBTable( $tableName ); $tableSchemaString = CargoUtils::getPageProp( $mainTemplatePageID, 'CargoFields' ); } else { $tableName = CargoUtils::getPageProp( $templatePageID, 'CargoTableName' ); } $tableSchema = CargoTableSchema::newFromDBString( $tableSchemaString ); // Then, match template params to Cargo table fields, by // parsing call(s) to #cargo_store. // Let's find every #cargo_store tag. // Unfortunately, it doesn't seem possible to use a regexp // search for this, because it's hard to know which set of // double brackets represents the end of such a call. Instead, // we'll do some manual parsing. $cargoStoreLocations = array(); $curPos = 0; while ( true ) { $newPos = strpos( $this->mTemplateText, "#cargo_store:", $curPos ); if ( $newPos === false ) { break; } $curPos = $newPos + 13; $cargoStoreLocations[] = $curPos; } $cargoStoreCalls = array(); foreach ( $cargoStoreLocations as $locNum => $startPos ) { $numUnclosedBrackets = 2; if ( $locNum < count( $cargoStoreLocations ) - 1 ) { $lastPos = $cargoStoreLocations[$locNum + 1]; } else { $lastPos = strlen( $this->mTemplateText ) - 1; } $curCargoStoreCall = ''; $curPos = $startPos; while ( $curPos <= $lastPos ) { $curChar = $this->mTemplateText[$curPos]; $curCargoStoreCall .= $curChar; if ( $curChar == '}' ) { $numUnclosedBrackets--; } elseif ( $curChar == '{' ) { $numUnclosedBrackets++; } if ( $numUnclosedBrackets == 0 ) { break; } $curPos++; } $cargoStoreCalls[] = $curCargoStoreCall; } foreach ( $cargoStoreCalls as $cargoStoreCall ) { if ( preg_match_all( '/([^|{]*?)=\s*{{{([^|}]*)/mis', $cargoStoreCall, $matches ) ) { foreach ( $matches[1] as $i => $cargoFieldName ) { $templateParameter = trim( $matches[2][$i] ); $cargoFieldsOfTemplateParams[$templateParameter] = $cargoFieldName; } } } // Now, combine the two sets of information into an array of // PFTemplateFields objects. $fieldDescriptions = $tableSchema->mFieldDescriptions; foreach ( $cargoFieldsOfTemplateParams as $templateParameter => $cargoField ) { $templateField = PFTemplateField::create( $templateParameter, $templateParameter ); if ( array_key_exists( $cargoField, $fieldDescriptions ) ) { $fieldDescription = $fieldDescriptions[$cargoField]; $templateField->setCargoFieldData( $tableName, $cargoField, $fieldDescription ); } $this->mTemplateFields[] = $templateField; } } public function getTemplateFields() { return $this->mTemplateFields; } public function getFieldNamed( $fieldName ) { foreach ( $this->mTemplateFields as $curField ) { if ( $curField->getFieldName() == $fieldName ) { return $curField; } } return null; } public function setConnectingProperty( $connectingProperty ) { $this->mConnectingProperty = $connectingProperty; } public function setCategoryName( $categoryName ) { $this->mCategoryName = $categoryName; } public function setAggregatingInfo( $aggregatingProperty, $aggregationLabel ) { $this->mAggregatingProperty = $aggregatingProperty; $this->mAggregationLabel = $aggregationLabel; } // Currently unused method. public function setFieldStartAndEnd( $fieldStart, $fieldEnd ) { $this->mFieldStart = $fieldStart; $this->mFieldEnd = $fieldEnd; } // Currently unused method. public function setTemplateStartAndEnd( $templateStart, $templateEnd ) { $this->mTemplateStart = $templateStart; $this->mTemplateEnd = $templateEnd; } public function setFormat( $templateFormat ) { $this->mTemplateFormat = $templateFormat; } public function createCargoDeclareCall() { $text = '{{#cargo_declare:'; $text .= '_table=' . $this->mCargoTable; foreach ( $this->mTemplateFields as $i => $field ) { if ( $field->getFieldType() == '' ) { continue; } $text .= '|'; $text .= str_replace( ' ', '_', $field->getFieldName() ) . '='; if ( $field->isList() ) { $delimiter = $field->getDelimiter(); if ( $delimiter == '' ) { $delimiter = ','; } $text .= "List ($delimiter) of "; } $text .= $field->getFieldType(); if ( $field->getHierarchyStructure() ) { $hierarchyStructureString = $field->getHierarchyStructure(); $text .= " (hierarchy;allowed values=$hierarchyStructureString)"; } elseif ( count( $field->getPossibleValues() ) > 0 ) { $allowedValuesString = implode( ',', $field->getPossibleValues() ); $text .= " (allowed values=$allowedValuesString)"; } } $text .= '}}'; return $text; } public function createCargoStoreCall() { $text = '{{#cargo_store:'; $text .= '_table=' . $this->mCargoTable; foreach ( $this->mTemplateFields as $i => $field ) { $text .= '|' . str_replace( ' ', '_', $field->getFieldName() ) . '={{{' . $field->getFieldName() . '|}}}'; } $text .= ' }}'; return $text; } /** * Creates the text of a template, when called from * Special:CreateTemplate, Special:CreateClass or the Page Schemas * extension. * @return string */ public function createText() { // Avoid PHP 7.1 warning from passing $this by reference $template = $this; Hooks::run( 'PageForms::CreateTemplateText', array( &$template ) ); $templateHeader = wfMessage( 'pf_template_docu', $this->mTemplateName )->inContentLanguage()->text(); $text = << $templateHeader

END;
		$text .= '{{' . $this->mTemplateName;
		if ( count( $this->mTemplateFields ) > 0 ) {
			$text .= "\n";
		}
		foreach ( $this->mTemplateFields as $field ) {
			if ( $field->getFieldName() == '' ) {
				continue;
			}
			$text .= "|" . $field->getFieldName() . "=\n";
		}
		if ( defined( 'CARGO_VERSION' ) && !defined( 'SMW_VERSION' ) && $this->mCargoTable != '' ) {
			$cargoInUse = true;
			$cargoDeclareCall = $this->createCargoDeclareCall() . "\n";
			$cargoStoreCall = $this->createCargoStoreCall();
		} else {
			$cargoInUse = false;
			$cargoDeclareCall = '';
			$cargoStoreCall = '';
		}

		$templateFooter = wfMessage( 'pf_template_docufooter' )->inContentLanguage()->text();
		$text .= <<
$templateFooter
$cargoDeclareCall$cargoStoreCall
END;

		// Before text
		$text .= $this->mTemplateStart;

		// $internalObjText can be either a call to #set_internal
		// or to #subobject (or null); which one we go with
		// depends on whether Semantic Internal Objects is installed,
		// and on the SMW version.
		// Thankfully, the syntaxes of #set_internal and #subobject
		// are quite similar, so we don't need too much extra logic.
		$internalObjText = null;
		if ( $this->mConnectingProperty ) {
			global $smwgDefaultStore;
			if ( defined( 'SIO_VERSION' ) ) {
				$useSubobject = false;
				$internalObjText = '{{#set_internal:' . $this->mConnectingProperty;
			} elseif ( $smwgDefaultStore == "SMWSQLStore3" ) {
				$useSubobject = true;
				$internalObjText = '{{#subobject:-|' . $this->mConnectingProperty . '={{PAGENAME}}';
			}
		}
		$setText = '';

		// Topmost part of table depends on format.
		if ( !$this->mTemplateFormat ) {
			$this->mTemplateFormat = 'standard';
		}
		if ( $this->mTemplateFormat == 'standard' ) {
			$tableText = '{| class="wikitable"' . "\n";
		} elseif ( $this->mTemplateFormat == 'infobox' ) {
			// A CSS style can't be used, unfortunately, since most
			// MediaWiki setups don't have an 'infobox' or
			// comparable CSS class.
			$tableText = <<{{PAGENAME}}
|-

END;
		} else {
			$tableText = '';
		}

		foreach ( $this->mTemplateFields as $i => $field ) {
			if ( $field->getFieldName() == '' ) {
				continue;
			}

			$fieldParam = '{{{' . $field->getFieldName() . '|}}}';
			if ( is_null( $field->getNamespace() ) ) {
				$fieldString = $fieldParam;
			} else {
				$fieldString = $field->getNamespace() . ':' . $fieldParam;
			}
			$separator = '';

			$fieldLabel = $field->getLabel();
			if ( $fieldLabel == '' ) {
				$fieldLabel = $field->getFieldName();
			}
			$fieldDisplay = $field->getDisplay();
			$fieldProperty = $field->getSemanticProperty();
			$fieldIsList = $field->isList();

			// Header/field label column
			if ( is_null( $fieldDisplay ) ) {
				if ( $this->mTemplateFormat == 'standard' || $this->mTemplateFormat == 'infobox' ) {
					if ( $i > 0 ) {
						$tableText .= "|-\n";
					}
					$tableText .= '! ' . $fieldLabel . "\n";
				} elseif ( $this->mTemplateFormat == 'plain' ) {
					$tableText .= "\n'''" . $fieldLabel . ":''' ";
				} elseif ( $this->mTemplateFormat == 'sections' ) {
					$tableText .= "\n==" . $fieldLabel . "==\n";
				}
			} elseif ( $fieldDisplay == 'nonempty' ) {
				if ( $this->mTemplateFormat == 'plain' || $this->mTemplateFormat == 'sections' ) {
					$tableText .= "\n";
				}
				$tableText .= '{{#if:' . $fieldParam . '|';
				if ( $this->mTemplateFormat == 'standard' || $this->mTemplateFormat == 'infobox' ) {
					if ( $i > 0 ) {
						$tableText .= "\n{{!}}-\n";
					}
					$tableText .= '! ' . $fieldLabel . "\n";
					$separator = '{{!}}';
				} elseif ( $this->mTemplateFormat == 'plain' ) {
					$tableText .= "'''" . $fieldLabel . ":''' ";
					$separator = '';
				} elseif ( $this->mTemplateFormat == 'sections' ) {
					$tableText .= '==' . $fieldLabel . "==\n";
					$separator = '';
				}
			} // If it's 'hidden', do nothing
			// Value column
			if ( $this->mTemplateFormat == 'standard' || $this->mTemplateFormat == 'infobox' ) {
				if ( $fieldDisplay == 'hidden' ) {
				} elseif ( $fieldDisplay == 'nonempty' ) {
					// $tableText .= "{{!}} ";
				} else {
					$tableText .= "| ";
				}
			}

			// If we're using Cargo, fields can simply be displayed
			// normally - no need for any special tags - *unless*
			// the field holds a list of Page value, in which case
			// we need to apply #arraymap.
			$isCargoListOfPages = $cargoInUse && $field->isList() && $field->getFieldType() == 'Page';
			if ( !$fieldProperty && !$isCargoListOfPages ) {
				if ( $separator != '' ) {
					$tableText .= "$separator ";
				}
				$tableText .= $this->createTextForField( $field );
				if ( $fieldDisplay == 'nonempty' ) {
					$tableText .= " }}";
				}
				$tableText .= "\n";
			} elseif ( !is_null( $internalObjText ) ) {
				if ( $separator != '' ) {
					$tableText .= "$separator ";
				}
				$tableText .= $this->createTextForField( $field );
				if ( $fieldDisplay == 'nonempty' ) {
					$tableText .= " }}";
				}
				$tableText .= "\n";
				if ( $field->isList() ) {
					if ( $useSubobject ) {
						$internalObjText .= '|' . $fieldProperty . '=' . $fieldString . '|+sep=,';
					} else {
						$internalObjText .= '|' . $fieldProperty . '#list=' . $fieldString;
					}
				} else {
					$internalObjText .= '|' . $fieldProperty . '=' . $fieldString;
				}
			} elseif ( $fieldDisplay == 'hidden' ) {
				if ( $fieldIsList ) {
					$setText .= $fieldProperty . '#list=' . $fieldString . '|';
				} else {
					$setText .= $fieldProperty . '=' . $fieldString . '|';
				}
			} elseif ( $fieldDisplay == 'nonempty' ) {
				if ( $this->mTemplateFormat == 'standard' || $this->mTemplateFormat == 'infobox' ) {
					$tableText .= '{{!}} ';
				}
				$tableText .= $this->createTextForField( $field ) . "\n";
			} else {
				$tableText .= $this->createTextForField( $field ) . "\n";
			}
		}

		// Add an inline query to the output text, for
		// aggregation, if a property was specified.
		if ( !is_null( $this->mAggregatingProperty ) && $this->mAggregatingProperty !== '' ) {
			if ( $this->mTemplateFormat == 'standard' || $this->mTemplateFormat == 'infobox' ) {
				if ( count( $this->mTemplateFields ) > 0 ) {
					$tableText .= "|-\n";
				}
				$tableText .= <<mAggregationLabel
| 
END;
			} elseif ( $this->mTemplateFormat == 'plain' ) {
				$tableText .= "\n'''" . $this->mAggregationLabel . ":''' ";
			} elseif ( $this->mTemplateFormat == 'sections' ) {
				$tableText .= "\n==" . $this->mAggregationLabel . "==\n";
			}
			$tableText .= "{{#ask:[[" . $this->mAggregatingProperty . "::{{SUBJECTPAGENAME}}]]|format=list}}\n";
		}
		if ( $this->mTemplateFormat == 'standard' || $this->mTemplateFormat == 'infobox' ) {
			$tableText .= "|}";
		}
		// Leave out newlines if there's an internal property
		// set here (which would mean that there are meant to be
		// multiple instances of this template.)
		if ( is_null( $internalObjText ) ) {
			if ( $this->mTemplateFormat == 'standard' || $this->mTemplateFormat == 'infobox' ) {
				$tableText .= "\n";
			}
		} else {
			$internalObjText .= "}}";
			$text .= $internalObjText;
		}

		// Add a call to #set, if necessary
		if ( $setText !== '' ) {
			$setText = '{{#set:' . $setText . "}}\n";
			$text .= $setText;
		}

		$text .= $tableText;
		if ( ( $this->mCategoryName !== '' ) && ( $this->mCategoryName !== null ) ) {
			global $wgContLang;
			$namespaceLabels = $wgContLang->getNamespaces();
			$categoryNamespace = $namespaceLabels[NS_CATEGORY];
			$text .= "\n[[$categoryNamespace:" . $this->mCategoryName . "]]\n";
		}

		// After text
		$text .= $this->mTemplateEnd;

		$text .= "\n";

		return $text;
	}

	function createTextForField( $field ) {
		$text = '';
		$fieldStart = $this->mFieldStart;
		Hooks::run( 'PageForms::TemplateFieldStart', array( $field, &$fieldStart ) );
		if ( $fieldStart != '' ) {
			$text .= "$fieldStart ";
		}

		$cargoInUse = defined( 'CARGO_VERSION' ) && !defined( 'SMW_VERSION' ) && $this->mCargoTable != '';
		$text .= $field->createText( $cargoInUse );

		$fieldEnd = $this->mFieldEnd;
		Hooks::run( 'PageForms::TemplateFieldEnd', array( $field, &$fieldEnd ) );
		if ( $fieldEnd != '' ) {
			$text .= " $fieldEnd";
		}

		return $text;
	}

}