Apr 042010
 

Suppose, you want to provide legacy URLs when porting the page to a new version. I need to do this, as I am writing the IdeaDay Import Wizard.

An example:

  • The new page will be under /my/new/page
  • You want a legacy alias under /legacy_path/legacy_name

There is a function $page->addCollectionAlias($c); which will alias your Collection UNDER another Collection (essentially adding the page with the same name elsewhere). We could use this, but would need to work around the new page's name and cHandle (which will change: page != legacy_name)

How does C5 handle this internally, when editing page aliases?

/concrete/startup/process.php contains the code (look for else if ($_POST['update_metadata']) and continue to read).

It essentially sets $data['ppURL'] to the aliases (new AND old), and uses $page->update($data);

There is one important thing to consider: to add a new path, the key needs to be a string! This had me baffled for one hour.

So, the entire code to add one new alias, AND keep all the old ones will be:

$this_page_paths = $in_pg->getPagePaths();
$new_paths = array();
foreach($this_page_paths as $path){
if (!$path['ppIsCanonical']) {
$new_paths[$path['ppID']] = $path['cPath'];
}
}
$new_paths['add_path'] = $add_alias;
$data = array();
$data['ppURL'] = $new_paths;
$in_pg->update($data);

$in_pg is the page you want to add the alias to. It is important that you check for the path being canonical, else your main page path will be overwritten (and the page will be redirecting to /). The new path ($add_alias) is added with a string (it does not matter which string you use).

We could use rescanPagePaths to do it directly, but we should not: update fires on_page_update events, for instance.

Now to the second part: modifying .htaccess to provide compatibility.

RewriteRule ^(.*)\.phtml(.*)$ $1$2 [L]

By adding this rule you will be stripping out .phtml from your links -> thus, compatibility with the following path from our example would be preserved:

/legacy_path/legacy_name.phtml

This rewriting is necessary, as Concrete does not allow dots (.) in page paths – it replaces them by an underscore.

Mar 312010
 

Fatal error: Class 'CollectionAttributeKey' not found in (…)/concrete/models/collection.php on line 183

This class is defined in concrete5.3.3.1\concrete\models\attribute\categories\collection.php

The problem is easily resolved by loading the appropriate model in your code:

Loader::model('attribute/categories/collection');

Mar 242010
 

The C5 API has a class Database in the core package (libraries/database.php). Alas, this is not the documentation we're looking for (we're looking for query, etc. methods, which are not present there).

Actually the methods used come directly from the included ADODB librarary. You can download a documentation here.

Usage:

//Load the Database Layer.
$db = Loader::db();
//quote the string we'll be inserting (use get_magic_quotes_gpc() as second parameter to avoid double escaping!)
$source = $db->qstr($source, get_magic_quotes_gpc());
$content = $db->qstr($content);
$sql = "insert into IdImportWizardSources (source, content) ";
$sql .= "values ($source,$content)";
if ($db->Execute($sql)===false) {
//error handling
echo 'error inserting: ' .$db->ErrorMsg();
}

Mar 232010
 

How does the installation of a db.xml file in the package root directory work?

I need to create tables for my Dashboard Application (Import Wizard), which will not be used by blocks – the Import Wizard does not contain any blocks.

In the Concrete5 forum, a function installDB was mentioned. It's defined in the InstallController class like this:

protected function installDB() {

$installDirectory = $this->installData['DIR_BASE_CORE'] . '/config';
$file = $installDirectory . '/db.xml';
if (!file_exists($file)) {
throw new Exception(t('Unable to locate database import file.'));
}

$db = Loader::db();
$db->ensureEncoding();
$err = Package::installDB($file);

}

It ensures the db.xml file exists and after some other setup steps and check finally calls Package::installDB (Package is a model):

public static function installDB($xmlFile) {

if (!file_exists($xmlFile)) {
return false;
}

// currently this is just done from xml

$db = Loader::db();

// this sucks – but adodb generates errors at the beginning because it attempts
// to find a table that doesn't exist!

$handler = $db->IgnoreErrors();
if ($db->getDebug() == false) {
ob_start();
}

$schema = $db->getADOSChema();
$sql = $schema->ParseSchema($xmlFile);

$db->IgnoreErrors($handler);

if (!$sql) {
$result->message = $db->ErrorMsg();
return $result;
}

$r = $schema->ExecuteSchema();

if ($db->getDebug() == false) {
$dbLayerErrorMessage = ob_get_contents();
ob_end_clean();
}

$result = new stdClass;
$result->result = false;

if ($dbLayerErrorMessage != ") {
$result->message = $dbLayerErrorMessage;
return $result;
} if (!$r) {
$result->message = $db->ErrorMsg();
return $result;
}

$result->result = true;

$db->CacheFlush();
return $result;

}

The Code is Copyright by Concrete5 Team.

It looks like I could just drop a db.xml file in my package directory and it will be installed. But how should I name my table?

By convention, there's different prefixes:

  • atX (atAddress, atBoolean, atDateTime) = attribute types
  • btX (btForm, btFlashContent, btSearch) = block types

But there's none for helper tables, to be used just for the Dashboard wizard.

Here's a link to the AdoDB XML Schemas (AXMLS) (the language db.xml is written in). I came up with the following:

<?xml version="1.0"?>
<schema version="0.3">
<table name="IdImportWizardSources">
<field name="ID" type="I">
<key />
<unsigned />
<autoincrement />
</field>
<field name="source" type="X" />
<field name="retrieved" type="T">
<deftimestamp />
</field>
<field name="content" type="B" />

</table>
</schema>

Here I define a new table named IdImportWizardSources (Id for IdeaDay), and several field values using keywords.

Notable is the retrieved field which is automatically set to the current timestamp (deftimestamp). content has a type of BLOB which should be able to store up to 4 GB with MySQL longblob type – unfortunately the amount you can store at once also depends on your MySQL max_packet_size. (Which is usually set to around 16 MB).

The table is imported and set up automatically by Concrete, without a single line o'code.

Mar 212010
 

concrete multi language

It is not enough to add a Character set to your Template // or store it in UTF-8 without BOM (so the headers attached by PHP won't break).

If TinyMCE only displays gibberish instead of the Russian / Chinese / … text you entered in edit mode, you should edit your .htaccess (located in the root directory of your site) and add the following:

AddDefaultCharset UTF-8

Optionally, while you're at it, you can enable caching for your site:

#Speed UP!
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 10 years"
ExpiresByType text/html "access plus 30 seconds"
</IfModule>

Mar 192010
 

Not understanding the Concrete 5 API comment on the select function of the Form Helper, I looked into the source:

/**
* Renders a select field. First argument is the name of the field. Second is an associative array of key => display. Second argument is either the value of the field to be selected (and if it's blank we check post) or a misc. array of fields
* @param string $key
* @return $html
*/
public function select($key, $values, $valueOrArray = false, $miscFields = array())

Select builds a SELECT Dopdown with OPTIONS. That much I already understood from the API.

Here's my explanation of the parameters passed in to select:

  • $key: the name of the element to be created.
  • $values: an array of options, stored as $k=>$value. Used like this:
    '<option value="' . $k . '" ' . $selected . '>' . $value . '</option>';
  • $valueOrArray: this can either be set to the default value to be selected (use the option value, not the displayed text between the tags) OR it can be an array and is used as the array to populate $miscFields (see explanation there), omitting the default value. A bit unusual perhaps, probably it's a timesaver when you code these forms a lot.
  • $miscFields: An associative array ($k => $value): insert additional keys into the tag (i.e. multiple="multiple"

The fun in using select is, it STORES your settings between sessions. Boy, I should have used it earlier, when hand-coding this for other parts of the project.

Another note on ValidationErrorHelper:

/**
* Returns whether or not this error helper has more than one error registered within it.
* @return bool
*/
public function has() {
return (count($this->error) > 0);
}

As one can read from the source above, the Validation Helper returns whether there are errors (even if it is only one) or there are none!

Mar 142010
 

To release a nice PDF helper, I want to include some fonts. As I want to leave the option open to SELL the PDF Helper, I have to make sure that the font licences allow me to do so.

For instance, the Microsoft Web Core Fonts package does not allow to package the fonts with a commercial product, thus increasing the value of the product.

First some file extensions demystified:

afm: Adobe Font Metrics file. Adds information to PostScript fonts (size, distance of individual characters, etc.) [German explanation]

pfb: PostScript Font Binary. (Postscript Type1) Includes outline information for the font. Goes with the pfm files.

pfm: PostScript Font Metrics file. Both files (pfb and pfm) are needed for Windows.

ttf: True Type Font. Developed by Apple Computers as an alternative to Postscript Type 1 fonts. They additionally support hinting (a technology to optimise screen viewing), thus they became the dominating standard.

Feb 222010
 

1004848 measuringWas mache ich?
Ich habe seit Oktober 2007 eine Firma (ideaday.de) im Bereich Internet / Telefonie – wir machen u.a.

  • dynamische Webseiten, die für die Kunden selbstständig und so einfach wie mit Word zu pflegen sind,
  • Telefonieanwendungen – bspw. interaktive Telefoniemenüs, 0800- und andere Servicenummern,
  • Mediendesign, bspw. Print-Layout von Broschüren bis hin zu Zeitungen

und natürlich auch Betreuung und Beratung rund um Computer/Medien im Raum München.

Falls Sie jemanden kennen sollten, der an diesen Bereichen Interesse hat, würde ich mich über eine Empfehlung nicht nur freuen, sondern Ihnen auch 10% vom ersten Umsatz des neuen Kunden als Dankeschön zukommen lassen!

Bild: forwardcom / sxc.hu

Jan 242010
 

I was trying to additionally rewrite an URL before letting the classical Concrete5 mod_rewrite happen – everything matched to index.php, like so:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule ^(.*)$ index.php/$1 [L]
</IfModule>

Íf you try to paste any additional RewriteRule before this catch-all rule, your Apache will run into an endless loop, probably giving up with a 500 error. Your log will show something like:

access.log

(…) "GET" "114.ideaday.de" "/stellenmarkt/stellenangebote/" "redirect:/index.php/index.php/index.php/index.php/index.php/index.php/index.php/index.php/index.php/index.php/index.php/stellenmarkt/stellenangebote/"
error.log

[Sun Jan 24 16:57:52 2010] [error] [client 85.181.108.54] Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.

Apparently mod_rewrite fails to recognize index.php as already being rewritten and keeps looping through the rule over and over again. Recent versions of Apache will stop after 10 iterations and yield a 500 error.
After nearly giving up, I've figured out a solution how to do this:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteRule ^stellenmarkt/stellengesuche/(.*)-([0-9]+)[/]*$ stellenmarkt/stellengesuche/gdemo/?id=$2 [L,QSA]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule ^(.*)$ index.php/$1 [L]
</IfModule>

As you see, the additional rule is placed before the Rewrite Conditions! This works, no more endless loops :-)