Skip to content
You are reading the docs for ShopClass 6.4.1, still in development. Read the stable docs.

Importing listings

A plugin that brings listings in from somewhere else should save them the way a posted listing is saved. Then validation, spam checks, custom fields, expiry, stats and hooks all apply, and the plugin writes no listing table itself.

That save path is ItemActions. It was written for the listing form, so it reads the request. prepareDataFrom() hands it plain data instead.

$actions = (new ItemActions(true))->asImport();
$actions->prepareDataFrom(array(
'title' => array('en_US' => 'Blue bike'),
'description' => array('en_US' => 'A good bike with lights.'),
'catId' => 12,
'price' => '150',
'currency' => 'EUR',
'contactName' => 'Partner shop',
'contactEmail' => '[email protected]',
'countryId' => 'DE',
'city' => 'Munich',
'ownerId' => 0,
'meta' => array(4 => 'Blue'),
'photos' => array('/tmp/import/bike-1.jpg'),
), true);
$result = $actions->add();
$itemId = Params::getParamInt('itemId');

add() returns 1 (saved, waiting for validation), 2 (saved and live) or an error message. On success the new id is in Params::getParamInt('itemId').

The keys are the names the listing form posts, plus:

Key What it is
meta Custom field values, keyed by field id
photos Paths to local image files. Each is re-encoded into the listing, then deleted
ownerId The account the listing belongs to; 0 for none
id For an edit: the listing to change

Without ownerId, admin mode gives the listing to the account whose e-mail is the contact e-mail, which is the admin editor’s rule. Pass ownerId whenever the contact address comes from outside.

new ItemActions(true) is admin mode: no posting wait, no e-mails, no listing limit and no moderation. ->asImport() keeps the first two off and turns the other two back on, so an import cannot skip the owner’s listing limit or the site’s approval queue.

$actions = (new ItemActions(true))->asImport();
$actions->prepareDataFrom(array('id' => $itemId) + $fields, false);
$result = $actions->edit();

An admin-mode edit from prepareDataFrom() needs no secret. The admin editor still checks the secret its form carries.

  • Params::withRequest($values, $fn) runs $fn with the request params replaced by $values, then puts the real ones back. Use it for other code that reads the request.
  • \mindstellar\location\LocationImporter::normalizeKey($name) gives a place name the key core matches locations by, so “München” and “munchen” find the same city.

Two classes in mindstellar\security cover what an API that receives listings needs.

RateLimit::hit($context, $key, $max, $windowSeconds) counts one request for $key and returns false once $max is passed in the window. The key can be an API key or an account id; it is stored hashed. If the counter cannot be reached, it allows the request.

if (!RateLimit::hit('acme_api', $apiKeyId, 120, 60)) {
// answer 429
}

AddressGuard checks an address that someone else sent before the server fetches it, for example a photo URL in a feed. It passes only http and https on their standard ports, and only when every IP the host resolves to is public.

$check = (new AddressGuard())->check($url);
if (!$check['ok']) {
// $check['error'] says why
}
// Connect to $check['ip'] with CURLOPT_RESOLVE, and do not follow redirects blindly.

Pin the download to the checked IP. A second DNS answer could otherwise point it at a private address. Check each redirect target the same way before following it.