In Mediawiki, how do I get the PageSaveComplete to execute? - mediawiki

Using Mediawiki 1.39 I have a number of hooks defined in a custom extension's extension.json file:
{
"name": "Dcycle Mediawiki Starterkit Custom Extension",
"version": "1.0.0",
"author": [
"Albert Albala"
],
"url": "http://github.com/dcycle/starterkit-mediawiki",
"descriptionmsg": "dcycle-mediawiki-starterkit-desc",
"license-name": "GPL-2.0-or-later",
"type": "other",
"requires": {
"MediaWiki": ">= 1.39.0"
},
"Hooks": {
"SpecialStatsAddExtra": "CustomExtension\\Hooks::onSpecialStatsAddExtra",
"PageSaveComplete": "CustomExtension\\Hooks::onPageSaveComplete",
"PageDeleteComplete": "CustomExtension\\Hooks::onPageDeleteComplete"
},
"JobClasses": {
"customExtensionUpdateWordCount": "CustomExtension\\Job\\UpdateWordCount"
},
"AutoloadNamespaces": {
"CustomExtension\\": "src/"
},
"MessagesDirs": {
"CustomExtension": [
"i18n"
]
},
"manifest_version": 2
}
The SpecialStatsAddExtra and PageDeleteComplete hooks get executed as expected, but the code in the PageSaveComplete hook never even gets called. I can put a syntax error in the CustomExtension/Hooks.php and no error happens when I save a page.
The PageSaveComplete seems to exist in MediaWiki 1.39, how can I make the hook be called when I save a page?
My hooks file looks like:
<?php
namespace CustomExtension;
use CustomExtension\JobScheduler\JobScheduler;
/**
* All CustomExtension's external hooks.
*/
class Hooks {
public static function onSpecialStatsAddExtra( &$extraStats, $context ) {
error_log('onSpecialStatsAddExtra is called', 3, "/var/tmp/my-errors.log");
return TRUE;
}
public static function onPageDeleteComplete(\MediaWiki\Page\ProperPageIdentity $page, \MediaWiki\Permissions\Authority $deleter, string $reason, int $pageID, \MediaWiki\Revision\RevisionRecord $deletedRev, \ManualLogEntry $logEntry, int $archivedRevisionCount) {
error_log('onPageDeleteComplete is called when I delete a page', 3, "/var/tmp/my-errors.log");
return TRUE;
}
public static function onPageSaveComplete(\WikiPage $wikiPage, \MediaWiki\User\UserIdentity $user, string $summary, int $flags, \MediaWiki\Revision\RevisionRecord $revisionRecord, \MediaWiki\Storage\EditResult $editResult) {
error_log('onPageSaveComplete is never called when I save a page', 3, "/var/tmp/my-errors.log");
return TRUE;
}
}

Related

How to verify stringified json in pact

I am trying to build a pact between two services using asynchronous communication.
This is the code I used for generate the pact:
#ExtendWith(PactConsumerTestExt.class)
#PactTestFor(providerName = "provider", providerType = ProviderType.ASYNCH)
public class StringifiedPactTest {
#Pact(consumer = "consumer", provider = "provider")
public MessagePact generatePact(MessagePactBuilder builder) {
return builder.hasPactWith("provider")
.expectsToReceive("A valid aws sns event")
.withContent(new PactDslJsonBody().stringType(new String[]{"MessageId", "TopicArn"}).stringValue("Message", new PactDslJsonBody().stringType("Value", "Foo").toString()))
.toPact();
}
#Test
#PactTestFor(pactMethod = "generatePact")
public void buildPact(List<Message> messages) {
}
}
And the generated pact is
{
"consumer": {
"name": "consumer"
},
"provider": {
"name": "provider"
},
"messages": [
{
"description": "A valid aws sns event",
"metaData": {
"contentType": "application/json"
},
"contents": {
"TopicArn": "string",
"Message": "{\"Value\":\"Foo\"}",
"MessageId": "string"
},
"matchingRules": {
"body": {
"$.MessageId": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.TopicArn": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.0.10"
}
}
}
This means the producer should have a "Message" that matches {"Value" : "Foo"}, any other combination like {"Value" : "Bar" } won't be successful.
Is there any way to add matching rules inside a stringified json?
Thanks!
Here's an anonymised example from a test we have. Hope it's useful. This creates a pact that matches only on type. So on the provider side, when I test against the contract, it doesn't matter what value I have for categoryName for example, as long as it's a stringType:
#PactTestFor(providerName = "provider-service", providerType = ProviderType.ASYNCH)
public class providerServiceConsumerPactTest {
private static String messageFromJson;
#BeforeAll
static void beforeAll() throws Exception {
messageFromJson = StreamUtils.copyToString(new ClassPathResource("/json/pact/consumer-service_provider-service.json").getInputStream(), Charset.defaultCharset());
}
#Pact(provider = "provider-service", consumer = "consumer-service")
public MessagePact providerServiceMessage(MessagePactBuilder builder) {
DslPart body = new PactDslJsonBody()
.object("metaData")
.stringType("origin", "provider-service")
.datetimeExpression("dateCreated", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.closeObject()
.minArrayLike("categories", 0, 1)
.stringType("id", "example data")
.stringType("categoryName", "example data")
.booleanType("clearance", false)
.closeObject()
.closeArray();
return builder
.expectsToReceive("a provider-service update")
.withContent(body)
.toPact();
}
#Test
#PactTestFor(pactMethod = "providerServiceMessage")
public void testProviderServiceMessage(MessagePact pact) {
// State
final String messageFromPact = pact.getMessages().get(0).contentsAsString();
// Assert
JSONAssert.assertEquals(messageFromPact, messageFromJson, false);
}
I'm having exactly the same issue, and unfortunately I don't think it's possible to tell Pact to parse the stringified JSON and look inside it (e.g. to verify that parse(Message).Value === "Foo" in your example).
The best you can do is write a regular expression to match the string you're expecting. This kind of sucks because there's no easy way to ignore the ordering of the JSON keys (e.g. "{\"a\":\"1\", \"b\":\"2\"}" and "{\"b\":\"2\", \"a\":\"1\"}" will compare different) but AFAIK Pact simply lacks the parsing functionality we're looking for, so the only tool it provides is regex.

.net Core and Serilog Email sink - json config

I'm using .net Core 2.0 and Serilog Email sink. I have problem to configure email sink with appsettings.json. The same configuration from program.cs is working while one from appsetting.json isn't.
The settings system (ReadFrom.Configuration()) really only does try to call methods and extension methods that it can discover and pass arguments provided from the configuration file.
Unfortunately, it only supports basic types for the time being (convertible to/from string and a few more specific cases) and therefore, parameters of type EmailConnectionInfo cannot be provided.
As a workaround, though, if you only need to pass in a few parameters, you can create your own extension method that accepts the parameters that you need and call it from the configuration system.
In your case, you would need to do the following :
First, define an extension method EmailCustom(...) that can be plugged on WriteTo (which is of type Serilog.Configuration.LoggerSinkConfiguration) and returns a LoggerConfiguration.
This would look something like (not tested, no usings etc :P) :
namespace Serilog{
public static class MyCustomExtensions
{
public static LoggerConfiguration EmailCustom(this LoggerSinkConfiguration sinkConfiguration, string param1, int param2, LogEventLevel restrictedToMinimumLevel){
// the actual call to configure the Email sink, passing in complex parameters
return sinkConfiguration.Email(... ... , restrictedToMinimumLevel , EmailConnectionInfo(){
Foo = "bar",
Baz = param1,
Qux = param2,
}
);
}
}
}
From that point on, you should be able to write C# code like :
new LoggerConfiguration()
.WriteTo.EmailCustom(param1: "my param1", param2: 42)
// ...
.CreateLogger();
Once you have that working, you can actually define that method call in json thanks to Serilog.Settings.Configuration
in that case, that would look like
{
"Serilog": {
"Using" : ["TheNameOfTheAssemblyThatContainsEmailCustom"],
"MinimumLevel": "Debug",
"WriteTo": [
{
"Name": "EmailCustom",
"Args": {
"param1": "my param1",
"param2": 42,
"restrictedToMinimumLevel": "Verbose"
}
}]
}
}
This strategy can be applied for other sinks and other configuration parts of Serilog as well.
You can find a bit more about the configuration system here :
the project's README Serilog.Settings.Configuration
examples of what can be done through configuration (shameless plug :p)
For others Like me that have trouble piecing things between the lines here is a complete answer using the framework presented by tsimbalar for a solution that sends email out using SendGrid.
I added the following class to the root of my project ("MyApp"). This gets called automatically from the ReadFrom.Configuration(configuration).CreateLogger(); due to the WriteTo EmailCustom in the appsettings.
using System;
using System.Net;
using Serilog;
using Serilog.Configuration;
using Serilog.Events;
using Serilog.Sinks.Email;
namespace TrackumApi
{
public static class SerilogEmailExtension
{
public static LoggerConfiguration EmailCustom(this LoggerSinkConfiguration sinkConfiguration,
string fromEmail,
string toEmail,
string enableSsl,
string mailSubject,
string isBodyHtml,
string mailServer,
string networkCredentialuserName,
string networkCredentialpassword,
string smtpPort,
string outputTemplate,
string batchPostingLimit,
string periodMinutes,
string restrictedToMinimumLevel)
{
return sinkConfiguration.Email(new EmailConnectionInfo
{
FromEmail = fromEmail,
ToEmail = toEmail,
EnableSsl = GetBoolean(enableSsl),
EmailSubject = mailSubject,
IsBodyHtml = GetBoolean(isBodyHtml),
MailServer = mailServer,
NetworkCredentials = new NetworkCredential(networkCredentialuserName, networkCredentialpassword),
Port = GetInt(smtpPort)
}, outputTemplate, GetLevel(restrictedToMinimumLevel),
GetInt(batchPostingLimit), TimeSpan.FromMinutes(GetInt(periodMinutes))
);
}
//The system hated converting the string inputs inline so I added the conversion methods:
private static int GetInt(string instring)
{
return int.TryParse(instring, out var result) ? result : 0;
}
private static bool GetBoolean(string instring)
{
return bool.TryParse(instring, out var result) && result;
}
private static LogEventLevel GetLevel(string restrictedtominimumlevel)
{
return Enum.TryParse(restrictedtominimumlevel, true,
out LogEventLevel level) ? level : LogEventLevel.Warning;
}
}
}
In my origianl post I modified my Program.cs but it turns out that is not needed. However the addition of the Serilog.Debugging.SelfLog before any other code is still priceless:
Serilog.Debugging.SelfLog.Enable(Console.Out);
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", true, true)
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
Finally I modified the appsettings.json as follows (forgive the extra, but I think that might also help somebody):
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Serilog": {
"Using": [ "Serilog", "Serilog.Sinks.Console", "Serilog.Sinks.File", "MyApp" ],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
"Microsoft": "Warning",
"System": "Warning",
"Microsoft.AspNetCore.Authentication": "Information"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss.fff} [{Level}] {SourceContext} {Message}{NewLine}{Exception}",
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
}
},
{
"Name": "File",
"Args": {
"path": "C:\\Temp\\Logs\\MyApp.log",
"fileSizeLimitBytes": 1000000,
"rollOnFileSizeLimit": "true",
"shared": "true",
"flushToDiskInterval": 3,
"outputTemplate": "[{Timestamp:MM/dd/yy HH:mm:ss} [{Level}] {SourceContext} {Message}{NewLine}{Exception}",
"restrictedToMinimumLevel": "Verbose"
}
},
{
"Name": "EmailCustom",
"Args": {
"fromEmail": "no-reply#mydomain.com",
"toEmail": "me#mydomain.com",
"enableSsl": false,
"mailSubject": "MyApp Message",
"isBodyHtml": true,
"mailServer": "smtp.sendgrid.net",
"networkCredentialuserName": "mysendgridusername",
"networkCredentialpassword": "mysendgridpassword",
"smtpPort": 587,
"outputTemplate": "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}",
"batchPostingLimit": 10,
"periodMinutes": 5,
"restrictedToMinimumLevel": "Verbose"
}
}
],
"Enrich": [ "FromLogContext" ],
"Properties": {
"Application": "MyApp"
}
}
}
HTH!

How to remove Task json properties in Nancy.Response.AsJson

I've made one of my API endpoints and inner logic asynchronous and when previously I've used Response.AsJson(Foo.bar()) , it would return the json representation normally, but now I see this appended to it:
{
"result": [
{
"id": "59d680cc734d1d08b4e6c89c",
"properties": {
"name": "value"
}
}
],
"id": 3,
"exception": null,
"status": 5,
"isCanceled": false,
"isCompleted": true,
"isCompletedSuccessfully": true,
"creationOptions": 0,
"asyncState": null,
"isFaulted": false
}
But I want it to be like this:
"id": "59d680cc734d1d08b4e6c89c",
"properties": {
"name": "value"
}
As I understand, it's because I've wrapped my object in a Task , but I can't figure out, how with Nancy framework, which I use the Response.AsJson, to make it so the properties are excluded. I can obviously omit the Response.AsJson of the returned object, but then response is no longer Json if requesting through web-browser for example.
For further example
NancyModule for routing API:
public ItemCatalogModule(IItemCatalog itemCatalog) : base("/itemCatalog")
{
Get("/fetch/{id}", async parameters =>
{
var id = (string) parameters.id;
var response = await Response.AsJson(itemCatalog.GetItem(id));
return response;
});
}
How the interface looks like of ItemCatalog:
public interface IItemCatalog
{
Task<Item> GetItem(string id);
}
You shoud do this :
public ItemCatalogModule(IItemCatalog itemCatalog) : base("/itemCatalog")
{
Get("/fetch/{id}", async parameters =>
{
var id = (string) parameters.id;
return Response.AsJson(await itemCatalog.GetItem(id));
});
}

How to set my own KeyGenerator instance in appsettings.json?

I already asked that question here but I feel like Stackoverflow might be faster. This is how I'm trying to do it in my json configuration file:
{
"Serilog": {
"Using": [ "Serilog.Sinks.AzureTableStorage" ],
"WriteTo": [
{
"Name": "AzureTableStorage",
"Args": {
"storageTableName": "Logs",
"connectionString": "*************",
"keyGenerator": "MyApp.Serilog.AzureTableStorage.MyKeyGenerator"
}
}
],
"MinimumLevel": "Verbose"
}
}
This is my generator implementation:
public class MyKeyGenerator : IKeyGenerator
{
public string GeneratePartitionKey(LogEvent logEvent)
{
return Environment.MachineName;
}
public string GenerateRowKey(LogEvent logEvent, string suffix = null)
{
return SUID.nextId().ToString();
}
}
Obviously, the .ReadFrom.Configuration operation throws an InvalidCastException, since it tries to fit the string content into an IKeyGenerator parameter.
How should I set the keyGenerator parameter to ensure an instance of MyKeyGenerator class is created and given to that parameter?
I cloned serilog-settings-configuration and after digging into the code, I found how they expect the JSON setting value when the actual parameter is an interface (See StringArgumentValue.cs, line 57 to 74).
The correct way to reference the type you want to pass as parameter is to give the full class and assembly names separated by a comma. That class must have a public default constructor as well.
Ex:
{
"Serilog": {
"Using": [ "Serilog.Sinks.AzureTableStorage" ],
"WriteTo": [
{
"Name": "AzureTableStorage",
"Args": {
"storageTableName": "Logs",
"connectionString": "*************",
"keyGenerator": "MyApp.Serilog.AzureTableStorage.MyKeyGenerator, MyApp"
}
}
],
"MinimumLevel": "Verbose"
}
}
That way, the configurator can instanciate the class properly!

Render Doctrine entities including recursive relations as JSON

I am building a JSON-based REST API using Symfony2.4 with Doctrine2.
EDIT : with JsonNormalizer, I can disabled some attributes, but what if I would like to set them, without recursivity ?
Basically, what I have (working) now is :
{
"tasks": [
{
"id": 1,
"name": "first task"
},
{
"id": 2,
"name": "second task"
}
]
}
What I would like is :
{
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category"
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category"
}
}
]
}
What was my initial problem is :
{
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category",
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...] // infinite...
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...] // infinite...
}
}
]
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category",
"tasks": [
{
"id": 1,
"name": "first task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...]
}
},
{
"id": 2,
"name": "second task",
"category": {
"id": 1,
"name": "first category",
"tasks": [...]
}
}
]
}
}
]
}
I have a entity A with a manyToOne relation to another entity B.
I have implemented the reverse-side, to be able to retrieve the related A entities on the B one.
class Task
{
/**
* #ORM\ManyToOne(targetEntity="List", inversedBy="task")
* #ORM\JoinColumn(name="list_id", referencedColumnName="id")
*/
private $list;
public function toArray($recursive = false)
{
$entityAsArray = get_object_vars($this);
if ($recursive) {
foreach ($entityAsArray as &$var) {
if ((is_object($var)) && (method_exists($var, 'toArray'))) {
$var = $var->toArray($recursive);
}
}
}
return $entityAsArray;
}
}
use Doctrine\Common\Collections\ArrayCollection;
class List
{
/**
* #ORM\OneToMany(targetEntity="Task", mappedBy="list")
*/
private $tasks;
public function __construct()
{
$this->tasks = new ArrayCollection();
}
}
Then I am building the different API routes and controllers,
Rendering the output as JsonResponses,
And I would like to render, for a given list, the different tasks using the route :
/api/v1/lists/1/tasks
The task action of my controller :
public function tasksAction($id)
{
$em = $this->getDoctrine()->getManager();
$list = $em->getRepository('MyRestBundle:List')->findOneActive($id);
if (!$list) {
throw $this->createNotFoundException('List undefined');
}
$tasks = $list->getTasks()->toArray();
foreach ($tasks as &$task) {
// recursively format the tasks as array
$task = $task->toArray(true);
}
$serializer = $this->get('serializer');
return $this->generateJsonResponse($serializer->normalize($tasks), 200);
}
But unfortunately, I always get a memory leak, because the call of toArray() is recursive, so each task has a list property which has a tasks collection etc.
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 130968 bytes) in src/Symfony/Component/Serializer/Serializer.php on line 146
I am wondering what would be the cleanest way to render entities with relations as JSON objects with Symfony2 ?
Do I really have to loop on my tasks to execute the "toArray()" method ?
I have also tried without it, without more success, except that the leak in in the file : src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php...
I have also tried without the JMSSeralizer, and the memory leak is thrown in my own php file.
Of course, I could increase the memory limit, but as it is an infinite loop problem of toArray() calls, it will not solve my problem.
How to format it properly ?
I have a feeling that we might be overthinking this. Would that work for you?
// In your controller
$repo = $this->getDoctrine()->getRepository('MyRestBundle:List');
$list = $repo->findActiveOne($id);
$tasks = $list->getTasks()->toArray();
$serializer = $this->get('serializer');
$json = $serializer->serialize($tasks, 'json');
Why is Task Entity recursive? A task can not include another task. Only a List can include an array of Tasks. So basically all we should do is get this array from the List entity and serialize it. Unless I am missing something.
EDIT:
You can ask the serializer to ignore certain attributes as mentioned by the documentation:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
$normalizer = new GetSetMethodNormalizer();
$normalizer->setIgnoredAttributes(array('age'));
$encoder = new JsonEncoder();
$serializer = new Serializer(array($normalizer), array($encoder));
$serializer->serialize($person, 'json'); // Output: {"name":"foo"}
Try to follow that example and just ignore the $list attribute in the Task entity.
EDIT2:
You don't need 'list' inside each task since it's the same. Your json should have 'list' and 'tasks' at the same level. then 'tasks' would be an array of tasks which will not contain 'list'. to achieve that you can have something like array('list' => $list, 'tasks' => $tasks) and serialize that.
EDIT
If I understand what your code is doing, I think the toArray($recursive) function goes into infinite recursion both directly (whenever $var = $this) and indirectly (i.e. by a sibling iterating through its own list and calling toArray of the original task again). Try keeping track of what's been processed to prevent infinite recursion:
/**
* #ORM\ManyToOne(targetEntity="List", inversedBy="task")
* #ORM\JoinColumn(name="list_id", referencedColumnName="id")
*/
private $list;
private $toArrayProcessed = array();
public function toArray($recursive = false)
{
$this->toArrayProcessed[$this->getId()] = 1;
$entityAsArray = get_object_vars($this);
if ($recursive) {
foreach ($entityAsArray as &$var) {
if ((is_object($var)) && (method_exists($var, 'toArray')) && !isset($this->toArrayProcessed[$var->getId()]) {
$var = $var->toArray($recursive);
}
}
}
return $entityAsArray;
}